Primera version de frontend
This commit is contained in:
373
src/pages/Admin.jsx
Normal file
373
src/pages/Admin.jsx
Normal file
@@ -0,0 +1,373 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
// Animación fade-in/slide-up para cards
|
||||
const fadeInSlideUp = `@keyframes fadein-slideup {
|
||||
0% { opacity: 0; transform: translateY(40px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}`;
|
||||
|
||||
// Inyectar animación global si no existe
|
||||
if (typeof document !== 'undefined' && !document.getElementById('fadein-slideup-admin')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'fadein-slideup-admin';
|
||||
style.innerHTML = fadeInSlideUp;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
import TestTailwind from '../components/TestTailwind';
|
||||
import { colors } from '../theme';
|
||||
|
||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||
|
||||
export default function Admin() {
|
||||
// Estado de servicios
|
||||
const [services, setServices] = useState(null);
|
||||
// Estado de descargas
|
||||
const [downloads, setDownloads] = useState(null);
|
||||
// Últimos documentos
|
||||
const [latestDocs, setLatestDocs] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
// Estado para análisis de actividad de usuario
|
||||
const [userActivity, setUserActivity] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const token = localStorage.getItem('access');
|
||||
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
|
||||
// Servicios
|
||||
const resServices = await fetch(`${API_URL}/cards/services-util-information/`, { headers });
|
||||
if (!resServices.ok) throw new Error('Error al obtener estados de servicios');
|
||||
const dataServices = await resServices.json();
|
||||
setServices(dataServices);
|
||||
|
||||
// Descargas
|
||||
const resDownloads = await fetch(`${API_URL}/cards/document-util-information/`, { headers });
|
||||
if (!resDownloads.ok) throw new Error('Error al obtener información de descargas');
|
||||
const dataDownloads = await resDownloads.json();
|
||||
setDownloads(dataDownloads);
|
||||
|
||||
// Últimos documentos
|
||||
const resDocs = await fetch(`${API_URL}/cards/downloaded-documents/`, { headers });
|
||||
if (!resDocs.ok) throw new Error('Error al obtener últimos documentos');
|
||||
const dataDocs = await resDocs.json();
|
||||
setLatestDocs(dataDocs.documentos);
|
||||
|
||||
// Análisis de actividad de usuario
|
||||
const resUserActivity = await fetch(`${API_URL}/cards/user-activity-analysis/`, { headers });
|
||||
if (!resUserActivity.ok) throw new Error('Error al obtener análisis de actividad de usuario');
|
||||
const dataUserActivity = await resUserActivity.json();
|
||||
setUserActivity(dataUserActivity);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
// Helper para nombre de archivo
|
||||
// Helper para nombre de archivo
|
||||
function getFileName(path) {
|
||||
return path.split('/').pop() || path;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header + Estado del Sistema alineados horizontalmente */}
|
||||
<div className="mb-8 flex flex-col md:flex-row md:items-stretch md:gap-6">
|
||||
{/* Header principal mejorado */}
|
||||
<div className="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 flex-1 min-w-0 md:w-[65%] animate-fadein-slideup opacity-0"
|
||||
style={{
|
||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards',
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0 bg-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">
|
||||
<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">
|
||||
Panel de Administración
|
||||
{services && (
|
||||
<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">
|
||||
{services.en_espera} en espera
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
<p className="text-lg text-blue-700/80 font-medium">
|
||||
{typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true'
|
||||
? 'Dashboard principal para gestión de Expediente electrónico'
|
||||
: 'Dashboard principal para gestión de agencia aduanal'}
|
||||
</p>
|
||||
</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>
|
||||
{/* Estado del Sistema card a la derecha */}
|
||||
<div className="mt-6 md:mt-0 md:w-[35%] min-w-[270px] flex-shrink-0 animate-fadein-slideup opacity-0"
|
||||
style={{
|
||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards',
|
||||
}}
|
||||
>
|
||||
<div className="relative overflow-hidden rounded-2xl shadow bg-gradient-to-br from-green-50 via-white to-blue-50 border border-green-100 p-6 h-full flex flex-col justify-between">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="bg-green-100 rounded-full p-3 shadow-md animate-bounce-slow">
|
||||
<svg className="h-7 w-7 text-green-600" 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" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-2xl font-extrabold text-green-900 tracking-tight flex-1">Estado del Sistema</h3>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-700 font-medium">API Backend</span>
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800 border border-green-200 gap-1">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
||||
Conectado
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-700 font-medium">API Servicios</span>
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800 border border-green-200 gap-1">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
||||
Conectado
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-700 font-medium">Última Actualización</span>
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800 border border-blue-200">
|
||||
Hace 2 min
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Efecto decorativo de fondo */}
|
||||
<div className="absolute -top-8 -right-8 opacity-20 pointer-events-none select-none">
|
||||
<svg width="80" height="80" viewBox="0 0 120 120" fill="none">
|
||||
<circle cx="60" cy="60" r="50" fill="url(#grad2)" />
|
||||
<defs>
|
||||
<linearGradient id="grad2" x1="0" y1="0" x2="120" y2="120" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#22c55e" stopOpacity="0.18" />
|
||||
<stop offset="1" stopColor="#3b82f6" stopOpacity="0.10" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards con datos de endpoints */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{/* Estados de servicios */}
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 hover:shadow-xl transition-all duration-300 transform hover:scale-105 animate-fadein-slideup opacity-0"
|
||||
style={{
|
||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.25s forwards',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center shadow-md">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="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>
|
||||
<div className="ml-5">
|
||||
<p className="text-sm font-medium text-gray-500">Procesos en Espera</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{services ? services.en_espera : '-'}</p>
|
||||
<p className="text-sm text-gray-400">Total: {services ? services.procesos_filtrados : '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 hover:shadow-xl transition-all duration-300 transform hover:scale-105 animate-fadein-slideup opacity-0"
|
||||
style={{
|
||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.35s forwards',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center shadow-md">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-5">
|
||||
<p className="text-sm font-medium text-gray-500">En Proceso</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{services ? services.en_proceso : '-'}</p>
|
||||
<p className="text-sm text-gray-400">Finalizados: {services ? services.finalizados : '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 hover:shadow-xl transition-all duration-300 transform hover:scale-105 animate-fadein-slideup opacity-0"
|
||||
style={{
|
||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.45s forwards',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-orange-500 to-orange-600 rounded-lg flex items-center justify-center shadow-md">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="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>
|
||||
<div className="ml-5">
|
||||
<p className="text-sm font-medium text-gray-500">Con Error</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{services ? services.con_error : '-'}</p>
|
||||
<p className="text-sm text-gray-400">Finalizados: {services ? services.finalizados : '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Descargas */}
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 hover:shadow-xl transition-all duration-300 transform hover:scale-105 animate-fadein-slideup opacity-0"
|
||||
style={{
|
||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.55s forwards',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-blue-700 to-blue-900 rounded-lg flex items-center justify-center shadow-md">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="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>
|
||||
<div className="ml-5">
|
||||
<p className="text-sm font-medium text-gray-500">Descargados 1 día</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{downloads ? downloads.archivos_ultimas_1_dia : '-'}</p>
|
||||
<p className="text-sm text-gray-400">7 días: {downloads ? downloads.archivos_ultimos_7_dias : '-'} | 30 días: {downloads ? downloads.archivos_ultimos_30_dias : '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Análisis de actividad de usuario */}
|
||||
{!(typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true') && (
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 mb-4 animate-fadein-slideup opacity-0"
|
||||
style={{
|
||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.65s forwards',
|
||||
}}
|
||||
>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">Actividad de Usuarios</h3>
|
||||
{loading ? (
|
||||
<div className="text-gray-500">Cargando...</div>
|
||||
) : error ? (
|
||||
<div className="text-danger-600">{error}</div>
|
||||
) : userActivity ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-700 mb-2">Resumen de acciones</h4>
|
||||
<ul className="text-sm text-gray-700 space-y-1">
|
||||
{Object.entries(userActivity.actions_count).map(([action, count]) => (
|
||||
<li key={action} className="flex justify-between border-b border-gray-100 py-1">
|
||||
<span className="capitalize">{action}</span>
|
||||
<span className="font-mono text-blue-700">{count}</span>
|
||||
</li>
|
||||
))}
|
||||
<li className="flex justify-between font-semibold pt-2">
|
||||
<span>Total actividades</span>
|
||||
<span className="font-mono text-blue-900">{userActivity.actividades_filtradas}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-700 mb-2">Top usuarios</h4>
|
||||
<ol className="text-sm text-gray-700 space-y-1 list-decimal list-inside">
|
||||
{userActivity.top_users.map((user, idx) => (
|
||||
<li key={user.username} className="flex justify-between border-b border-gray-100 py-1">
|
||||
<span>{user.username}</span>
|
||||
<span className="font-mono text-green-700">{user.activity_count}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tabla de últimos documentos */}
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 mb-8 animate-fadein-slideup opacity-0"
|
||||
style={{
|
||||
animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.75s forwards',
|
||||
}}
|
||||
>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">Últimos documentos agregados</h3>
|
||||
{loading ? (
|
||||
<div className="text-gray-500">Cargando...</div>
|
||||
) : error ? (
|
||||
<div className="text-danger-600">{error}</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left font-semibold text-gray-600">Archivo</th>
|
||||
<th className="px-4 py-2 text-left font-semibold text-gray-600">Pedimento</th>
|
||||
<th className="px-4 py-2 text-left font-semibold text-gray-600">Organización</th>
|
||||
<th className="px-4 py-2 text-left font-semibold text-gray-600">Fecha</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{latestDocs.map(doc => (
|
||||
<tr key={doc.id} className="hover:bg-blue-50">
|
||||
<td className="px-4 py-2 font-mono text-blue-800 truncate max-w-xs" title={getFileName(doc.archivo)}>{getFileName(doc.archivo)}</td>
|
||||
<td className="px-4 py-2">{doc.pedimento}</td>
|
||||
<td className="px-4 py-2">{doc.organizacion}</td>
|
||||
<td className="px-4 py-2">{new Date(doc.created_at).toLocaleString('es-MX')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
src/pages/Debug.jsx
Normal file
13
src/pages/Debug.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Debug() {
|
||||
return (
|
||||
<div style={{ padding: '20px', backgroundColor: '#f0f0f0', minHeight: '100vh' }}>
|
||||
<h1 style={{ color: 'red', fontSize: '24px' }}>🐛 Debug Page</h1>
|
||||
<p>Si ves esto, React está funcionando correctamente.</p>
|
||||
<p>Fecha y hora: {new Date().toLocaleString()}</p>
|
||||
<p>Token en localStorage: {localStorage.getItem('access') ? 'SÍ' : 'NO'}</p>
|
||||
<p>URL actual: {window.location.href}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
645
src/pages/Documents.jsx
Normal file
645
src/pages/Documents.jsx
Normal file
@@ -0,0 +1,645 @@
|
||||
import React, { useEffect, useState, useLayoutEffect, useRef } from 'react';
|
||||
import SuccessModal from '../components/SuccessModal.jsx';
|
||||
// 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-documents')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'fadein-slideup-documents';
|
||||
style.innerHTML = fadeInSlideUp;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
import { fetchPedimentoDocuments } from '../api/documentos.ts';
|
||||
import { useNotification } from '../context/NotificationContext';
|
||||
// import { usePolling } from '../hooks/usePolling';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||
|
||||
// Descarga individual
|
||||
const downloadFile = async (id, filename = 'archivo', setSuccess, setError, showMessage) => {
|
||||
const token = localStorage.getItem('access');
|
||||
const res = await fetch(`${API_URL}/record/documents/descargar/${id}/`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (res.status === 401) {
|
||||
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
alert('No autorizado o error en la descarga');
|
||||
return;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
if (setSuccess) setSuccess('Descarga exitosa');
|
||||
};
|
||||
|
||||
// Descarga masiva (bulk)
|
||||
const downloadBulkZip = async (ids, showMessage, setSuccess, nombreZip = 'documentos') => {
|
||||
if (!ids.length) {
|
||||
showMessage('Selecciona al menos un documento.', 'error');
|
||||
return;
|
||||
}
|
||||
const token = localStorage.getItem('access');
|
||||
const res = await fetch(`${API_URL}/record/documents/bulk-download/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ document_ids: ids, pedimento_nombre: nombreZip }),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
showMessage('No autorizado o error en la descarga masiva', 'error');
|
||||
return;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${nombreZip || 'documentos'}.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
if (setSuccess) setSuccess('Descarga(s) completada(s)');
|
||||
};
|
||||
|
||||
export default function Documents() {
|
||||
const focusKeeperRef = useRef(null);
|
||||
const [success, setSuccess] = useState('');
|
||||
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
const [extensionFilter, setExtensionFilter] = useState('');
|
||||
const [documentTypeFilter, setDocumentTypeFilter] = useState('');
|
||||
const [createdAtFilter, setCreatedAtFilter] = useState('');
|
||||
const [pedimentoNumeroFilter, setPedimentoNumeroFilter] = useState('');
|
||||
const { showMessage } = useNotification();
|
||||
// Estado para controlar la animación de entrada
|
||||
const [showAnimation, setShowAnimation] = useState(false);
|
||||
const [hasAnimated, setHasAnimated] = useState(false);
|
||||
useLayoutEffect(() => {
|
||||
// Forzar un render antes de activar la animación
|
||||
setShowAnimation(true);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (showAnimation && !hasAnimated) {
|
||||
const timeout = setTimeout(() => {
|
||||
setHasAnimated(true);
|
||||
setShowAnimation(false);
|
||||
}, 700); // Duración igual a la animación
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [showAnimation, hasAnimated]);
|
||||
|
||||
// Estado local para los datos, loading y error
|
||||
const [docsData, setDocsData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// Fetch de datos solo al cargar la página o cuando cambian los filtros/paginación
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const fetchDocsData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const token = localStorage.getItem('access');
|
||||
const data = await fetchPedimentoDocuments(token, '', currentPage, itemsPerPage, {
|
||||
pedimento_numero: pedimentoNumeroFilter,
|
||||
extension: extensionFilter,
|
||||
document_type: documentTypeFilter,
|
||||
created_at: createdAtFilter,
|
||||
});
|
||||
if (isMounted) setDocsData(data);
|
||||
} catch (err) {
|
||||
if (isMounted) setError(err);
|
||||
} finally {
|
||||
if (isMounted) setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchDocsData();
|
||||
return () => { isMounted = false; };
|
||||
}, [currentPage, itemsPerPage, pedimentoNumeroFilter, extensionFilter, documentTypeFilter, createdAtFilter]);
|
||||
|
||||
// Refetch manual (si se quiere usar en el futuro)
|
||||
const refetch = () => {
|
||||
setCurrentPage(1); // Esto forzará el useEffect a recargar
|
||||
};
|
||||
|
||||
// Manejo de errores de sesión
|
||||
useEffect(() => {
|
||||
if (error && error.message === 'SESSION_EXPIRED') {
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
} else if (error) {
|
||||
showMessage(error.message, 'error');
|
||||
}
|
||||
}, [error, showMessage]);
|
||||
|
||||
|
||||
// Cálculos de paginación usando la estructura tipada
|
||||
const documentsArray = docsData && docsData.results ? docsData.results : [];
|
||||
const totalDocuments = docsData && typeof docsData.count === 'number' ? docsData.count : 0;
|
||||
const totalPages = totalDocuments > 0 ? Math.ceil(totalDocuments / itemsPerPage) : 1;
|
||||
const currentDocuments = documentsArray;
|
||||
|
||||
// Selección de documentos
|
||||
const [selectedDocs, setSelectedDocs] = useState([]);
|
||||
// allSelected: todos los docs de la página actual están seleccionados
|
||||
const allSelected = currentDocuments.length > 0 && selectedDocs.length === currentDocuments.length;
|
||||
// someSelected: hay al menos uno seleccionado pero no todos
|
||||
const someSelected = selectedDocs.length > 0 && selectedDocs.length < currentDocuments.length;
|
||||
|
||||
// Handlers para selección
|
||||
const handleSelectOne = (id) => {
|
||||
setSelectedDocs(prev => prev.includes(id) ? prev.filter(d => d !== id) : [...prev, id]);
|
||||
};
|
||||
const handleSelectAll = () => {
|
||||
if (allSelected) {
|
||||
setSelectedDocs([]);
|
||||
} else {
|
||||
setSelectedDocs(currentDocuments.map(doc => doc.id));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Descargar seleccionados (bulk) con prompt para nombre del zip
|
||||
const handleDownloadSelected = async () => {
|
||||
const ids = currentDocuments.filter(doc => selectedDocs.includes(doc.id)).map(doc => doc.id);
|
||||
if (ids.length === 1) {
|
||||
// Si solo hay uno, descarga individual
|
||||
const doc = currentDocuments.find(doc => doc.id === ids[0]);
|
||||
await downloadFile(doc.id, doc.archivo ? doc.archivo.split('/').pop() : 'archivo', () => {
|
||||
setSuccess('Descarga exitosa');
|
||||
setShowSuccessModal(true);
|
||||
}, null, showMessage);
|
||||
} else if (ids.length > 1) {
|
||||
let nombreZip = window.prompt('¿Qué nombre quieres para el archivo zip?', 'documentos_seleccionados');
|
||||
if (!nombreZip) nombreZip = 'documentos_seleccionados';
|
||||
await downloadBulkZip(ids, showMessage, () => {
|
||||
setSuccess('Descarga exitosa');
|
||||
setShowSuccessModal(true);
|
||||
}, nombreZip);
|
||||
}
|
||||
};
|
||||
|
||||
// Descargar todos los de la página (bulk) con prompt para nombre del zip
|
||||
const handleDownloadAll = async () => {
|
||||
const ids = currentDocuments.map(doc => doc.id);
|
||||
if (ids.length === 1) {
|
||||
const doc = currentDocuments[0];
|
||||
await downloadFile(doc.id, doc.archivo ? doc.archivo.split('/').pop() : 'archivo', () => {
|
||||
setSuccess('Descarga exitosa');
|
||||
setShowSuccessModal(true);
|
||||
}, null, showMessage);
|
||||
} else if (ids.length > 1) {
|
||||
let nombreZip = window.prompt('¿Qué nombre quieres para el archivo zip?', 'documentos_pagina');
|
||||
if (!nombreZip) nombreZip = 'documentos_pagina';
|
||||
await downloadBulkZip(ids, showMessage, () => {
|
||||
setSuccess('Descarga exitosa');
|
||||
setShowSuccessModal(true);
|
||||
}, nombreZip);
|
||||
}
|
||||
};
|
||||
|
||||
// Limpiar selección al cambiar de página o documentos
|
||||
useEffect(() => {
|
||||
setSelectedDocs([]);
|
||||
}, [currentPage, itemsPerPage, pedimentoNumeroFilter, extensionFilter, documentTypeFilter, createdAtFilter, docsData]);
|
||||
|
||||
// Obtener lista única de contribuyentes para el combobox (de la página actual)
|
||||
const contribuyentes = Array.from(new Set(currentDocuments.map(d => d.contribuyente).filter(Boolean)));
|
||||
|
||||
// Refuerza la paginación SPA: nunca recarga la página, solo cambia el estado local
|
||||
const handlePageChange = (newPage, e) => {
|
||||
if (e && typeof e.preventDefault === 'function') e.preventDefault();
|
||||
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
|
||||
if (newPage < 1 || newPage > totalPages || newPage === currentPage) return;
|
||||
setCurrentPage(newPage);
|
||||
// Quitar el foco del botón activo para evitar salto de scroll
|
||||
if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
|
||||
// Forzar foco al div invisible para evitar saltos por enfoque automático
|
||||
useLayoutEffect(() => {
|
||||
if (focusKeeperRef.current) {
|
||||
focusKeeperRef.current.focus();
|
||||
}
|
||||
}, [currentPage]);
|
||||
|
||||
const handleItemsPerPageChange = (newItemsPerPage) => {
|
||||
setItemsPerPage(newItemsPerPage);
|
||||
setCurrentPage(1); // Reset a la primera página
|
||||
};
|
||||
|
||||
|
||||
// El layout principal y la tabla siempre se renderizan, loader/error/empty solo dentro del área de la tabla
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-50">
|
||||
<div ref={focusKeeperRef} tabIndex={-1} style={{position:'absolute',width:0,height:0,overflow:'hidden',outline:'none'}} aria-hidden="true"></div>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header mejorado y 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"+
|
||||
(showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '')
|
||||
}
|
||||
style={showAnimation && !hasAnimated ? { animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' } : undefined}>
|
||||
<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">
|
||||
<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">
|
||||
Documentos
|
||||
<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">{totalDocuments}</span>
|
||||
</h1>
|
||||
<p className="text-lg text-blue-700/80 font-medium">Descarga los documentos de tus pedimentos.</p>
|
||||
</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>
|
||||
</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 className={
|
||||
"bg-white shadow-lg rounded-xl border border-gray-200"+
|
||||
(showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '')
|
||||
}
|
||||
style={showAnimation && !hasAnimated ? { animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' } : undefined}>
|
||||
<div className="px-6 py-6 border-b border-gray-200">
|
||||
<div className="overflow-x-auto" id="tabla-documentos">
|
||||
{/* Header de Documentos Relacionados arriba de los filtros */}
|
||||
<div className="px-8 pt-8 pb-2 border-b border-gray-200">
|
||||
<h2 className="text-2xl font-extrabold text-blue-800 tracking-tight mb-1">
|
||||
Todos los Documentos
|
||||
</h2>
|
||||
<div className="h-1 w-10 bg-blue-400 rounded mb-2"></div>
|
||||
</div>
|
||||
{/* Filtros de query parameters */}
|
||||
<div className="px-6 py-6 border-b border-gray-200">
|
||||
{/* Filtros avanzados */}
|
||||
<div className="mb-4 flex flex-wrap gap-4 items-end justify-between">
|
||||
{/* Pedimento Número */}
|
||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Pedimento Número</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pedimentoNumeroFilter}
|
||||
onChange={e => setPedimentoNumeroFilter(e.target.value)}
|
||||
placeholder="Buscar por número de pedimento..."
|
||||
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-gray-50"
|
||||
/>
|
||||
</div>
|
||||
{/* Extensión */}
|
||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
||||
<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-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-gray-50"
|
||||
>
|
||||
<option value="">Todas</option>
|
||||
<option value="pdf">PDF</option>
|
||||
<option value="xml">XML</option>
|
||||
<option value="jpg">JPG</option>
|
||||
<option value="jpeg">JPEG</option>
|
||||
<option value="png">PNG</option>
|
||||
<option value="xls">XLS</option>
|
||||
<option value="xlsx">XLSX</option>
|
||||
<option value="doc">DOC</option>
|
||||
<option value="docx">DOCX</option>
|
||||
<option value="txt">TXT</option>
|
||||
<option value="zip">ZIP</option>
|
||||
<option value="rar">RAR</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* Tipo de documento */}
|
||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Tipo de documento</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-gray-50"
|
||||
>
|
||||
<option value="">Todos</option>
|
||||
<option value="1">Pedimento Partida</option>
|
||||
<option value="2">Pedimento Completo</option>
|
||||
<option value="3">Pedimento Remesas</option>
|
||||
<option value="4">Pedimento Acuse</option>
|
||||
<option value="5">Pedimento EDocument</option>
|
||||
<option value="6">Estado Pedimento</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* Fecha de creación */}
|
||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Fecha de creación</label>
|
||||
<input
|
||||
type="date"
|
||||
value={createdAtFilter}
|
||||
onChange={e => setCreatedAtFilter(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-gray-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Botón de actualizar eliminado por solicitud */}
|
||||
<SuccessModal open={showSuccessModal} onClose={() => setShowSuccessModal(false)} message={success || 'Descarga exitosa'} />
|
||||
{/* Botones de descarga */}
|
||||
{currentDocuments.length > 0 && (
|
||||
<div className="flex space-x-3 mb-2">
|
||||
<button
|
||||
onClick={handleDownloadAll}
|
||||
disabled={currentDocuments.length === 0}
|
||||
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"
|
||||
>
|
||||
<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 todos
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownloadSelected}
|
||||
disabled={selectedDocs.length === 0}
|
||||
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"
|
||||
>
|
||||
<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" />
|
||||
</svg>
|
||||
Descargar seleccionados ({selectedDocs.length})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ minHeight: 'calc(6 * 56px)', maxHeight: 'calc(6 * 56px)', overflowY: currentDocuments.length > 6 ? 'auto' : 'hidden', position: 'relative' }}>
|
||||
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden text-xs">
|
||||
<thead className="bg-gradient-to-r from-gray-50 sticky top-0 z-20">
|
||||
<tr>
|
||||
<th className="px-2 py-2 text-center font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSelected}
|
||||
ref={el => { if (el) el.indeterminate = someSelected; }}
|
||||
onChange={handleSelectAll}
|
||||
className="h-3.5 w-3.5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded align-middle"
|
||||
style={{ minWidth: '14px', minHeight: '14px' }}
|
||||
/>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Pedimento</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Archivo</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Tipo</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Tamaño</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Extensión</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-100" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
|
||||
{/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */}
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={10} 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>
|
||||
) : error ? (
|
||||
<tr>
|
||||
<td colSpan={10} 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: {error.message || 'Error al cargar documentos'}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : currentDocuments.length > 0 ? (
|
||||
<>
|
||||
{currentDocuments.map(doc => (
|
||||
<tr key={doc.id} className="transition-all duration-200 hover:bg-blue-100 hover:shadow-lg">
|
||||
<td className="px-2 py-2 text-center align-middle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedDocs.includes(doc.id)}
|
||||
onChange={() => handleSelectOne(doc.id)}
|
||||
className="h-3.5 w-3.5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded align-middle"
|
||||
style={{ minWidth: '14px', minHeight: '14px' }}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle font-medium text-blue-900">{doc.pedimento_numero}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-800">{doc.archivo ? doc.archivo.split('/').pop() : ''}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{
|
||||
(() => {
|
||||
switch (String(doc.document_type)) {
|
||||
case '1': return 'Pedimento Partida';
|
||||
case '2': return 'Pedimento Completo';
|
||||
case '3': return 'Pedimento Remesas';
|
||||
case '4': return 'Pedimento Acuse';
|
||||
case '5': return 'Pedimento EDocument';
|
||||
case '6': return 'Estado Pedimento';
|
||||
default: return doc.document_type || '';
|
||||
}
|
||||
})()
|
||||
}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{doc.size}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{doc.extension}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center align-middle">
|
||||
<button
|
||||
className="inline-flex items-center px-3 py-1 border border-transparent text-xs font-semibold 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"
|
||||
title="Descargar"
|
||||
onClick={async () => {
|
||||
await downloadFile(
|
||||
doc.id,
|
||||
doc.archivo ? doc.archivo.split('/').pop() : 'archivo',
|
||||
() => {
|
||||
setSuccess('Descarga exitosa');
|
||||
setShowSuccessModal(true);
|
||||
},
|
||||
null,
|
||||
showMessage
|
||||
);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{/* Rellenar con filas vacías si hay menos de 8 */}
|
||||
{currentDocuments.length < 8 && !loading && !error && Array.from({length: 8 - currentDocuments.length}).map((_, idx) => (
|
||||
<tr key={`empty-${idx}`} className="">
|
||||
<td className="px-2 py-4" />
|
||||
<td className="px-6 py-4 whitespace-nowrap" colSpan={5}> </td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={10} 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">No hay pedimentos</h3>
|
||||
<p className="text-gray-500">Aún no tienes pedimentos registrados.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botón de actualizar eliminado por solicitud */}
|
||||
<SuccessModal open={showSuccessModal} onClose={() => setShowSuccessModal(false)} message={success || 'Descarga exitosa'} />
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden">
|
||||
|
||||
{/* Paginación con botones numerados y elipsis */}
|
||||
{totalDocuments > 0 && (
|
||||
<div className="bg-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
|
||||
{(() => {
|
||||
const totalPages = Math.max(1, Math.ceil(totalDocuments / itemsPerPage));
|
||||
const maxPagesToShow = 5;
|
||||
let startPage = Math.max(1, currentPage - 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="itemsPerPage" className="text-xs text-gray-600 font-medium">Registros por página:</label>
|
||||
<select
|
||||
id="itemsPerPage"
|
||||
value={itemsPerPage}
|
||||
onChange={e => handleItemsPerPageChange(Number(e.target.value))}
|
||||
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"
|
||||
>
|
||||
{[5, 10, 20, 50, 100, 200, 400, 800, 1200, 2400, 10000].map(size => (
|
||||
<option key={size} value={size}>{size}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={e => handlePageChange(1, e)}
|
||||
disabled={currentPage === 1}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${currentPage === 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
|
||||
type="button"
|
||||
onClick={e => handlePageChange(currentPage - 1, e)}
|
||||
disabled={currentPage === 1}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${currentPage === 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
|
||||
type="button"
|
||||
key={num}
|
||||
onClick={e => handlePageChange(num, e)}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${num === currentPage ? '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 === currentPage}
|
||||
>
|
||||
{num}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={e => handlePageChange(currentPage + 1, e)}
|
||||
disabled={currentPage >= totalPages}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(currentPage >= 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
|
||||
type="button"
|
||||
onClick={e => handlePageChange(totalPages, e)}
|
||||
disabled={currentPage >= totalPages}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(currentPage >= 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">{currentPage}</span> de <span className="font-bold">{totalPages}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
578
src/pages/Expedientes.jsx
Normal file
578
src/pages/Expedientes.jsx
Normal file
@@ -0,0 +1,578 @@
|
||||
import React, { useEffect, useState, useLayoutEffect, useRef } 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-documents')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'fadein-slideup-documents';
|
||||
style.innerHTML = fadeInSlideUp;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
import { fetchDocuments } from '../api/expedientes.ts';
|
||||
import { useNotification } from '../context/NotificationContext';
|
||||
import { usePolling } from '../hooks/usePolling';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||
|
||||
const downloadFile = async (id, filename = 'archivo', setSuccess, setError, showMessage) => {
|
||||
const token = localStorage.getItem('access');
|
||||
const res = await fetch(`${API_URL}/record/documents/descargar/${id}/`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (res.status === 401) {
|
||||
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
alert('No autorizado o error en la descarga');
|
||||
return;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
if (setSuccess) setSuccess('Descarga exitosa');useEffect
|
||||
};
|
||||
|
||||
export default function Documents() {
|
||||
const focusKeeperRef = useRef(null);
|
||||
const [success, setSuccess] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
const [alertaFilter, setAlertaFilter] = useState('all'); // all, true, false
|
||||
const [expedienteFilter, setExpedienteFilter] = useState('all'); // all, true, false
|
||||
const [contribuyenteFilter, setContribuyenteFilter] = useState('');
|
||||
const [contribuyenteInput, setContribuyenteInput] = useState('');
|
||||
const [fechaPagoFilter, setFechaPagoFilter] = useState('');
|
||||
const [pedimentoFilter, setPedimentoFilter] = useState('');
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
const [curpApoderadoFilter, setCurpApoderadoFilter] = useState('');
|
||||
const [patenteFilter, setPatenteFilter] = useState('');
|
||||
const [aduanaFilter, setAduanaFilter] = useState('');
|
||||
const [tipoOperacionFilter, setTipoOperacionFilter] = useState('');
|
||||
const [clavePedimentoFilter, setClavePedimentoFilter] = useState('');
|
||||
const { showMessage } = useNotification();
|
||||
// Estado para controlar la animación de entrada
|
||||
const [showAnimation, setShowAnimation] = useState(false);
|
||||
const [hasAnimated, setHasAnimated] = useState(false);
|
||||
useLayoutEffect(() => {
|
||||
// Forzar un render antes de activar la animación
|
||||
setShowAnimation(true);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (showAnimation && !hasAnimated) {
|
||||
const timeout = setTimeout(() => {
|
||||
setHasAnimated(true);
|
||||
setShowAnimation(false);
|
||||
}, 700); // Duración igual a la animación
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [showAnimation, hasAnimated]);
|
||||
|
||||
// Fetching usando la función tipada de TypeScript
|
||||
const fetchPedimentosData = async (page = currentPage, pageSize = itemsPerPage) => {
|
||||
const token = localStorage.getItem('access');
|
||||
// Construir objeto de filtros
|
||||
const filters = {
|
||||
search: searchFilter || undefined,
|
||||
pedimento: pedimentoFilter || undefined,
|
||||
existe_expediente: expedienteFilter === 'all' ? undefined : expedienteFilter,
|
||||
alerta: alertaFilter === 'all' ? undefined : alertaFilter,
|
||||
contribuyente: contribuyenteFilter || undefined,
|
||||
curp_apoderado: curpApoderadoFilter || undefined,
|
||||
fecha_pago: fechaPagoFilter || undefined,
|
||||
patente: patenteFilter || undefined,
|
||||
aduana: aduanaFilter || undefined,
|
||||
tipo_operacion: tipoOperacionFilter || undefined,
|
||||
clave_pedimento: clavePedimentoFilter || undefined,
|
||||
};
|
||||
return await fetchDocuments(token, page, pageSize, filters);
|
||||
};
|
||||
|
||||
// Hook de polling que se ejecuta cada 30 segundos
|
||||
const { data: pedimentos, loading, error, refetch } = usePolling(
|
||||
() => fetchPedimentosData(currentPage, itemsPerPage),
|
||||
30000, // 30 segundos
|
||||
[currentPage, itemsPerPage, searchFilter, pedimentoFilter, expedienteFilter, alertaFilter, contribuyenteFilter, curpApoderadoFilter, fechaPagoFilter, patenteFilter, aduanaFilter, tipoOperacionFilter, clavePedimentoFilter]
|
||||
);
|
||||
|
||||
// Manejo de errores de sesión
|
||||
useEffect(() => {
|
||||
if (error && error.message === 'SESSION_EXPIRED') {
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
} else if (error) {
|
||||
showMessage(error.message, 'error');
|
||||
}
|
||||
}, [error, showMessage]);
|
||||
|
||||
// Cálculos de paginación usando la estructura tipada
|
||||
const documentsArray = pedimentos && pedimentos.results ? pedimentos.results : [];
|
||||
const totalDocuments = pedimentos && typeof pedimentos.count === 'number' ? pedimentos.count : 0;
|
||||
const totalPages = totalDocuments > 0 ? Math.ceil(totalDocuments / itemsPerPage) : 1;
|
||||
const currentDocuments = documentsArray;
|
||||
|
||||
// Obtener lista única de contribuyentes para el combobox (de la página actual)
|
||||
const contribuyentes = Array.from(new Set(currentDocuments.map(d => d.contribuyente).filter(Boolean)));
|
||||
|
||||
|
||||
// Refuerza la paginación SPA: nunca recarga la página, solo cambia el estado local
|
||||
const handlePageChange = (newPage, e) => {
|
||||
if (e && typeof e.preventDefault === 'function') e.preventDefault();
|
||||
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
|
||||
if (newPage < 1 || newPage > totalPages || newPage === currentPage) return;
|
||||
setCurrentPage(newPage);
|
||||
// Quitar el foco del botón activo para evitar salto de scroll
|
||||
if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
|
||||
// Forzar foco al div invisible para evitar saltos por enfoque automático
|
||||
useLayoutEffect(() => {
|
||||
if (focusKeeperRef.current) {
|
||||
focusKeeperRef.current.focus();
|
||||
}
|
||||
}, [currentPage]);
|
||||
|
||||
const handleItemsPerPageChange = (newItemsPerPage) => {
|
||||
setItemsPerPage(newItemsPerPage);
|
||||
setCurrentPage(1); // Reset a la primera página
|
||||
};
|
||||
|
||||
|
||||
// El layout principal y la tabla siempre se renderizan, loader/error/empty solo dentro del área de la tabla
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-50">
|
||||
<div ref={focusKeeperRef} tabIndex={-1} style={{position:'absolute',width:0,height:0,overflow:'hidden',outline:'none'}} aria-hidden="true"></div>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header mejorado y 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"+
|
||||
(showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '')
|
||||
}
|
||||
style={showAnimation && !hasAnimated ? { animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' } : undefined}>
|
||||
<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">
|
||||
<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">
|
||||
Expedientes
|
||||
<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">{totalDocuments}</span>
|
||||
</h1>
|
||||
<p className="text-lg text-blue-700/80 font-medium">Gestiona y descarga los documentos de tus pedimentos.</p>
|
||||
</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>
|
||||
</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 className={
|
||||
"bg-white shadow-lg rounded-xl border border-gray-200"+
|
||||
(showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '')
|
||||
}
|
||||
style={showAnimation && !hasAnimated ? { animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' } : undefined}>
|
||||
<div className="px-6 py-6 border-b border-gray-200">
|
||||
{/* Filtros avanzados */}
|
||||
<div className="mb-4 flex flex-wrap gap-4 items-end">
|
||||
{/* Search global */}
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Buscar</label>
|
||||
<input
|
||||
type="text"
|
||||
value={searchFilter}
|
||||
onChange={e => setSearchFilter(e.target.value)}
|
||||
placeholder="Buscar pedimento, contribuyente, agente aduanal..."
|
||||
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>
|
||||
{/* Pedimento */}
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Pedimento</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pedimentoFilter}
|
||||
onChange={e => setPedimentoFilter(e.target.value)}
|
||||
placeholder="Buscar pedimento..."
|
||||
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>
|
||||
{/* Alerta */}
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Alerta</label>
|
||||
<select value={alertaFilter} onChange={e => setAlertaFilter(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">
|
||||
<option value="all">Todos</option>
|
||||
<option value="true">Sí</option>
|
||||
<option value="false">No</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* Expediente */}
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Expediente</label>
|
||||
<select value={expedienteFilter} onChange={e => setExpedienteFilter(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">
|
||||
<option value="all">Todos</option>
|
||||
<option value="true">Sí</option>
|
||||
<option value="false">No</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* Contribuyente combobox */}
|
||||
<div className="flex flex-col relative">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Contribuyente</label>
|
||||
<input
|
||||
type="text"
|
||||
value={contribuyenteInput}
|
||||
onChange={e => {
|
||||
setContribuyenteInput(e.target.value);
|
||||
setContribuyenteFilter('');
|
||||
}}
|
||||
placeholder="Buscar o escribir..."
|
||||
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"
|
||||
autoComplete="off"
|
||||
/>
|
||||
{/* Dropdown de sugerencias */}
|
||||
{contribuyenteInput && (
|
||||
<div className="absolute top-14 left-0 w-44 bg-white border border-gray-200 rounded-lg shadow-lg z-10 max-h-40 overflow-auto">
|
||||
{contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).length === 0 ? (
|
||||
<div className="px-3 py-2 text-sm text-gray-500">Sin coincidencias</div>
|
||||
) : (
|
||||
contribuyentes.filter(c => c.toLowerCase().includes(contribuyenteInput.toLowerCase())).map(c => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
className="w-full text-left px-3 py-2 hover:bg-blue-50 text-sm"
|
||||
onClick={() => {
|
||||
setContribuyenteFilter(c);
|
||||
setContribuyenteInput('');
|
||||
}}
|
||||
>
|
||||
{c}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* CURP Apoderado */}
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">CURP Apoderado</label>
|
||||
<input
|
||||
type="text"
|
||||
value={curpApoderadoFilter}
|
||||
onChange={e => setCurpApoderadoFilter(e.target.value)}
|
||||
placeholder="CURP del apoderado..."
|
||||
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>
|
||||
{/* Fecha de pago */}
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Fecha de pago</label>
|
||||
<input type="date" value={fechaPagoFilter} onChange={e => setFechaPagoFilter(e.target.value)}
|
||||
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>
|
||||
{/* Patente */}
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Patente</label>
|
||||
<input
|
||||
type="text"
|
||||
value={patenteFilter}
|
||||
onChange={e => setPatenteFilter(e.target.value)}
|
||||
placeholder="Patente..."
|
||||
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>
|
||||
{/* Aduana */}
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Aduana</label>
|
||||
<input
|
||||
type="text"
|
||||
value={aduanaFilter}
|
||||
onChange={e => setAduanaFilter(e.target.value)}
|
||||
placeholder="Aduana..."
|
||||
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>
|
||||
{/* Tipo de operación */}
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Tipo de operación</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tipoOperacionFilter}
|
||||
onChange={e => setTipoOperacionFilter(e.target.value)}
|
||||
placeholder="ID tipo operación..."
|
||||
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>
|
||||
{/* Clave pedimento */}
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Clave pedimento</label>
|
||||
<input
|
||||
type="text"
|
||||
value={clavePedimentoFilter}
|
||||
onChange={e => setClavePedimentoFilter(e.target.value)}
|
||||
placeholder="Clave pedimento..."
|
||||
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>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-500 bg-gray-100 px-3 py-1 rounded-full">
|
||||
🔄 Actualización automática cada 30 segundos
|
||||
</span>
|
||||
<button
|
||||
onClick={refetch}
|
||||
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"
|
||||
>
|
||||
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Actualizar Ahora
|
||||
</button>
|
||||
|
||||
</div>
|
||||
{success && (
|
||||
<div className="mt-4 bg-green-50 border border-green-200 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<svg className="h-5 w-5 text-green-400 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
<p className="text-green-800">{success}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden">
|
||||
<div className="overflow-x-auto" id="tabla-documentos">
|
||||
<div style={{ minHeight: 'calc(7 * 56px)', maxHeight: 'calc(7 * 56px)', overflowY: currentDocuments.length > 8 ? 'auto' : 'hidden', position: 'relative' }}>
|
||||
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden sticky text-xs">
|
||||
<thead className="bg-gray-50 sticky top-0 z-20 shadow">
|
||||
<tr>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Pedimento</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Fecha de pago</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Contribuyente</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Alerta</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">CURP Apoderado</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Importe total</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Saldo disponible</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Importe pedimento</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Expediente</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
|
||||
{/* Loader/Error/Empty state dentro del área de la tabla, sin cambiar el layout */}
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={9} 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>
|
||||
) : error ? (
|
||||
<tr>
|
||||
<td colSpan={9} 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: {error.message || 'Error al cargar documentos'}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : currentDocuments.length > 0 ? (
|
||||
<>
|
||||
{currentDocuments.map(ped => (
|
||||
<tr key={ped.id} className="hover:bg-blue-50 transition-all duration-300 hover:scale-[1.02] hover:shadow-md">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<Link
|
||||
to={`/expedientes/pedimento/${ped.id}`}
|
||||
className="text-blue-600 hover:text-blue-800 font-medium transition-colors duration-200"
|
||||
>
|
||||
{ped.pedimento}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{ped.fechapago}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{ped.contribuyente}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
ped.alerta
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{ped.alerta ? 'Sí' : 'No'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{ped.curp_apoderado}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">${ped.importe_total}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">${ped.saldo_disponible}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">${ped.importe_pedimento}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
ped.existe_expediente
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{ped.existe_expediente ? 'Sí' : 'No'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{/* Rellenar con filas vacías si hay menos de 8 */}
|
||||
{currentDocuments.length < 8 && !loading && !error && Array.from({length: 8 - currentDocuments.length}).map((_, idx) => (
|
||||
<tr key={`empty-${idx}`} className="">
|
||||
<td className="px-6 py-4 whitespace-nowrap" colSpan={9}> </td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={9} 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">No hay pedimentos</h3>
|
||||
<p className="text-gray-500">Aún no tienes pedimentos registrados.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/* Paginación con botones numerados y elipsis */}
|
||||
{totalDocuments > 0 && (
|
||||
<div className="bg-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
|
||||
{(() => {
|
||||
const totalPages = Math.max(1, Math.ceil(totalDocuments / itemsPerPage));
|
||||
const maxPagesToShow = 5;
|
||||
let startPage = Math.max(1, currentPage - 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="itemsPerPage" className="text-xs text-gray-600 font-medium">Registros por página:</label>
|
||||
<select
|
||||
id="itemsPerPage"
|
||||
value={itemsPerPage}
|
||||
onChange={e => handleItemsPerPageChange(Number(e.target.value))}
|
||||
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"
|
||||
>
|
||||
{[5, 10, 20, 50, 100, 200, 400, 800, 1200, 2400, 10000].map(size => (
|
||||
<option key={size} value={size}>{size}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={e => handlePageChange(1, e)}
|
||||
disabled={currentPage === 1}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${currentPage === 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
|
||||
type="button"
|
||||
onClick={e => handlePageChange(currentPage - 1, e)}
|
||||
disabled={currentPage === 1}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${currentPage === 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
|
||||
type="button"
|
||||
key={num}
|
||||
onClick={e => handlePageChange(num, e)}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${num === currentPage ? '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 === currentPage}
|
||||
>
|
||||
{num}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={e => handlePageChange(currentPage + 1, e)}
|
||||
disabled={currentPage >= totalPages}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(currentPage >= 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
|
||||
type="button"
|
||||
onClick={e => handlePageChange(totalPages, e)}
|
||||
disabled={currentPage >= totalPages}
|
||||
className={`px-2 py-1 rounded border text-xs font-semibold transition-colors duration-150 ${(currentPage >= 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">{currentPage}</span> de <span className="font-bold">{totalPages}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
195
src/pages/ForgotPassword.jsx
Normal file
195
src/pages/ForgotPassword.jsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||
|
||||
export default function ForgotPassword() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setSuccess(false);
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/user/password-reset/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, email }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
setError(data.detail || 'No se pudo enviar el correo.');
|
||||
} else {
|
||||
setSuccess(true);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Error de red. Intenta de nuevo.');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#F2F4F7' }}>
|
||||
{/* Background pattern */}
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
<div className="absolute inset-0" style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fillRule='evenodd'%3E%3Cg fill='%231B2A41' fillOpacity='0.1'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`
|
||||
}}></div>
|
||||
</div>
|
||||
<div className="relative max-w-md w-full">
|
||||
{/* Main Card */}
|
||||
<div className="bg-white backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200 overflow-hidden">
|
||||
{/* Header with navy background */}
|
||||
<div className="px-8 py-10 text-center" style={{ backgroundColor: '#1B2A41' }}>
|
||||
<div className="mb-4">
|
||||
<a href="/" className="inline-block">
|
||||
<h1 className="text-4xl font-bold text-white">
|
||||
EFC
|
||||
</h1>
|
||||
</a>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">
|
||||
Recuperar contraseña
|
||||
</h2>
|
||||
<p className="text-sm" style={{ color: 'rgba(255, 255, 255, 0.8)' }}>
|
||||
Ingresa tu usuario y correo para recibir el enlace de recuperación
|
||||
</p>
|
||||
</div>
|
||||
{/* Form */}
|
||||
<div className="px-8 py-8">
|
||||
{success ? (
|
||||
<div className="text-green-700 bg-green-50 border border-green-200 rounded p-4 text-center mb-4">
|
||||
Si el correo está registrado, recibirás un enlace para restablecer tu contraseña.
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
|
||||
Usuario
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5" style={{ color: '#7A7A7A' }} 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>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:border-transparent transition duration-200"
|
||||
style={{ color: '#333333', borderColor: '#d1d5db' }}
|
||||
placeholder="Tu usuario"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
onFocus={e => {
|
||||
e.target.style.borderColor = 'transparent';
|
||||
e.target.style.boxShadow = '0 0 0 2px #4DA6FF';
|
||||
}}
|
||||
onBlur={e => {
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
if (document.activeElement !== e.target) {
|
||||
e.target.style.borderColor = '#4DA6FF';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
if (document.activeElement !== e.target) {
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
|
||||
Correo electrónico
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5" style={{ color: '#7A7A7A' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 12H8m8 0a4 4 0 11-8 0 4 4 0 018 0zm-4 4v2m0-6V8" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:border-transparent transition duration-200"
|
||||
style={{ color: '#333333', borderColor: '#d1d5db' }}
|
||||
placeholder="tucorreo@ejemplo.com"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
onFocus={e => {
|
||||
e.target.style.borderColor = 'transparent';
|
||||
e.target.style.boxShadow = '0 0 0 2px #4DA6FF';
|
||||
}}
|
||||
onBlur={e => {
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
if (document.activeElement !== e.target) {
|
||||
e.target.style.borderColor = '#4DA6FF';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
if (document.activeElement !== e.target) {
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{error && <div className="rounded-xl bg-red-50 border p-4 animate-pulse text-danger-600 text-sm text-center" style={{ borderColor: 'rgba(198, 40, 40, 0.2)' }}>{error}</div>}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
||||
style={{ backgroundColor: '#1B2A41', '--tw-ring-color': '#1B2A41' }}
|
||||
onMouseEnter={e => {
|
||||
if (!loading) {
|
||||
e.target.style.backgroundColor = '#162234';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
if (!loading) {
|
||||
e.target.style.backgroundColor = '#1B2A41';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" 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>
|
||||
Enviando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Enviar enlace de recuperación</span>
|
||||
<svg className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
<div className="mt-6 text-center">
|
||||
<a href="/login" className="text-blue-600 hover:underline text-sm">Volver al inicio de sesión</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Floating elements */}
|
||||
<div className="absolute -top-4 -left-4 w-24 h-24 rounded-full blur-xl" style={{ backgroundColor: 'rgba(255, 255, 255, 0.1)' }}></div>
|
||||
<div className="absolute -bottom-4 -right-4 w-32 h-32 rounded-full blur-xl" style={{ backgroundColor: 'rgba(27, 42, 65, 0.2)' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
427
src/pages/Importers.jsx
Normal file
427
src/pages/Importers.jsx
Normal file
@@ -0,0 +1,427 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export default function Importers() {
|
||||
const [importers, setImporters] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
|
||||
// Datos dummy para mostrar
|
||||
const dummyImporters = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Importadora ABC S.A.',
|
||||
rfc: 'ABC123456789',
|
||||
email: 'contacto@abc.com',
|
||||
status: 'Activo',
|
||||
lastActivity: '2024-01-15',
|
||||
documentsCount: 45
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Comercial XYZ Ltda.',
|
||||
rfc: 'XYZ987654321',
|
||||
email: 'info@xyz.com',
|
||||
status: 'Activo',
|
||||
lastActivity: '2024-01-14',
|
||||
documentsCount: 23
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Global Trade Corp.',
|
||||
rfc: 'GTC555666777',
|
||||
email: 'admin@globaltrade.com',
|
||||
status: 'Inactivo',
|
||||
lastActivity: '2024-01-10',
|
||||
documentsCount: 12
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Simular carga de datos
|
||||
const timer = setTimeout(() => {
|
||||
setImporters(dummyImporters);
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const filteredImporters = importers.filter(importer =>
|
||||
importer.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
importer.rfc.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
importer.email.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
// Cálculos de paginación
|
||||
const totalImporters = filteredImporters.length;
|
||||
const totalPages = Math.ceil(totalImporters / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const currentImporters = filteredImporters.slice(startIndex, endIndex);
|
||||
|
||||
// Reset página cuando cambia el filtro
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [searchTerm]);
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const handleItemsPerPageChange = (newItemsPerPage) => {
|
||||
setItemsPerPage(newItemsPerPage);
|
||||
setCurrentPage(1); // Reset a la primera página
|
||||
};
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
return status === 'Activo'
|
||||
? 'bg-success-100 text-success-800 border border-success-200'
|
||||
: 'bg-danger-100 text-danger-800 border border-danger-200';
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="h-full bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<svg className="animate-spin h-12 w-12 text-navy-600 mx-auto mb-4" 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>
|
||||
<p className="text-gray-600 text-lg">Cargando información de importadores...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6">
|
||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-navy-900 to-navy-700 bg-clip-text text-transparent mb-2">
|
||||
Importadores
|
||||
</h1>
|
||||
<p className="text-gray-600">Gestiona y supervisa las empresas importadoras registradas en el sistema.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-white overflow-hidden shadow-xl rounded-xl border border-gray-200">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-navy-500 to-navy-600 rounded-lg flex items-center justify-center shadow-lg">
|
||||
<svg className="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Total Importadores</dt>
|
||||
<dd className="text-2xl font-bold text-navy-900">{importers.length}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white overflow-hidden shadow-xl rounded-xl border border-gray-200">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-success-500 to-success-600 rounded-lg flex items-center justify-center shadow-lg">
|
||||
<svg className="h-6 w-6 text-white" 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" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Activos</dt>
|
||||
<dd className="text-2xl font-bold text-success-900">
|
||||
{importers.filter(i => i.status === 'Activo').length}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white overflow-hidden shadow-xl rounded-xl border border-gray-200">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-danger-500 to-danger-600 rounded-lg flex items-center justify-center shadow-lg">
|
||||
<svg className="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Inactivos</dt>
|
||||
<dd className="text-2xl font-bold text-danger-900">
|
||||
{importers.filter(i => i.status === 'Inactivo').length}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white overflow-hidden shadow-xl rounded-xl border border-gray-200">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-primary-500 to-primary-600 rounded-lg flex items-center justify-center shadow-lg">
|
||||
<svg className="h-6 w-6 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>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Total Documentos</dt>
|
||||
<dd className="text-2xl font-bold text-primary-900">
|
||||
{importers.reduce((sum, i) => sum + i.documentsCount, 0)}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search and Actions */}
|
||||
<div className="bg-white shadow-xl rounded-xl border border-gray-200 mb-8">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" 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>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="focus:ring-primary-500 focus:border-primary-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-lg shadow-sm transition-all duration-200"
|
||||
placeholder="Buscar importadores..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 sm:mt-0 sm:ml-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200 transform hover:scale-105"
|
||||
>
|
||||
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
Nuevo Importador
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gradient-to-r from-gray-50 to-gray-100">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Importador
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
RFC
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Estado
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Documentos
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
||||
Última Actividad
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-4">
|
||||
<span className="sr-only">Acciones</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{currentImporters.map((importer, index) => (
|
||||
<tr key={importer.id} className={`hover:bg-gray-50 transition-colors duration-200 ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}`}>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 h-12 w-12">
|
||||
<div className="h-12 w-12 rounded-full bg-gradient-to-br from-navy-500 to-navy-600 flex items-center justify-center shadow-lg">
|
||||
<svg className="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="text-sm font-semibold text-gray-900">{importer.name}</div>
|
||||
<div className="text-sm text-gray-500">{importer.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{importer.rfc}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex px-3 py-1 text-xs font-semibold rounded-full ${getStatusBadge(importer.status)}`}>
|
||||
{importer.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm font-semibold text-gray-900">{importer.documentsCount}</span>
|
||||
<span className="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800">
|
||||
docs
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(importer.lastActivity).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button className="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md text-white bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 transition-all duration-200 transform hover:scale-105">
|
||||
Ver
|
||||
</button>
|
||||
<button className="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md text-white bg-gradient-to-r from-warning-600 to-warning-700 hover:from-warning-700 hover:to-warning-800 transition-all duration-200 transform hover:scale-105">
|
||||
Editar
|
||||
</button>
|
||||
<button className="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md text-white bg-gradient-to-r from-danger-600 to-danger-700 hover:from-danger-700 hover:to-danger-800 transition-all duration-200 transform hover:scale-105">
|
||||
Eliminar
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Paginación */}
|
||||
{totalImporters > 0 && (
|
||||
<div className="bg-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
|
||||
<div className="flex items-center mb-4 sm:mb-0">
|
||||
<span className="text-sm text-gray-700 mr-4">
|
||||
Mostrando <span className="font-semibold">{startIndex + 1}</span> - <span className="font-semibold">{Math.min(endIndex, totalImporters)}</span> de <span className="font-semibold">{totalImporters}</span> importadores
|
||||
</span>
|
||||
<select
|
||||
value={itemsPerPage}
|
||||
onChange={(e) => handleItemsPerPageChange(Number(e.target.value))}
|
||||
className="border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-navy-500 focus:border-navy-500"
|
||||
>
|
||||
<option value={10}>10 por página</option>
|
||||
<option value={15}>15 por página</option>
|
||||
<option value={20}>20 por página</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="relative inline-flex items-center px-3 py-2 rounded-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Anterior
|
||||
</button>
|
||||
|
||||
<div className="hidden sm:flex space-x-1">
|
||||
{[...Array(totalPages)].map((_, index) => {
|
||||
const page = index + 1;
|
||||
const isCurrentPage = page === currentPage;
|
||||
const isNearCurrentPage = Math.abs(page - currentPage) <= 2;
|
||||
const isFirstOrLast = page === 1 || page === totalPages;
|
||||
|
||||
if (totalPages <= 7 || isNearCurrentPage || isFirstOrLast) {
|
||||
return (
|
||||
<button
|
||||
key={page}
|
||||
onClick={() => handlePageChange(page)}
|
||||
className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium rounded-md transition-colors ${
|
||||
isCurrentPage
|
||||
? 'z-10 bg-navy-600 border-navy-600 text-white'
|
||||
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
);
|
||||
} else if (page === currentPage - 3 || page === currentPage + 3) {
|
||||
return (
|
||||
<span key={page} className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
|
||||
...
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="sm:hidden flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-700">
|
||||
Página {currentPage} de {totalPages}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="relative inline-flex items-center px-3 py-2 rounded-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Siguiente
|
||||
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Empty state */}
|
||||
{currentImporters.length === 0 && !loading && (
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-12">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto h-24 w-24 bg-gradient-to-br from-gray-100 to-gray-200 rounded-full flex items-center justify-center mb-6">
|
||||
<svg className="h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">No se encontraron importadores</h3>
|
||||
<p className="text-gray-500 mb-6">
|
||||
{searchTerm ? 'Intenta con otros términos de búsqueda.' : 'Comienza agregando un nuevo importador.'}
|
||||
</p>
|
||||
{!searchTerm && (
|
||||
<button className="inline-flex items-center px-4 py-2 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200 transform hover:scale-105">
|
||||
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
Agregar primer importador
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
785
src/pages/Landing.jsx
Normal file
785
src/pages/Landing.jsx
Normal file
@@ -0,0 +1,785 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function Landing() {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [activeSection, setActiveSection] = useState('inicio');
|
||||
const [contactForm, setContactForm] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
company: '',
|
||||
message: ''
|
||||
});
|
||||
|
||||
// Efecto de scroll para navbar
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 50);
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
// Smooth scroll para navegación
|
||||
const scrollToSection = (sectionId) => {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
setActiveSection(sectionId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContactSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
alert('Gracias por tu mensaje. Nos pondremos en contacto contigo pronto.');
|
||||
setContactForm({ name: '', email: '', company: '', message: '' });
|
||||
};
|
||||
|
||||
// Estadísticas animadas
|
||||
const stats = [
|
||||
{ number: '500+', label: 'Agentes Aduanales', icon: '🏢' },
|
||||
{ number: '15,000+', label: 'Pedimentos Procesados', icon: '📋' },
|
||||
{ number: '99.9%', label: 'Uptime Garantizado', icon: '⚡' },
|
||||
{ number: '24/7', label: 'Soporte Especializado', icon: '🛡️' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* Navbar flotante con efectos */}
|
||||
<header className={`fixed top-0 w-full z-50 transition-all duration-300 ${
|
||||
isScrolled
|
||||
? 'bg-white/95 backdrop-blur-md shadow-lg border-b border-gray-200'
|
||||
: 'bg-transparent'
|
||||
}`}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center py-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<h1 className="text-2xl font-bold">
|
||||
<span className="bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
|
||||
EFC
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<nav className="hidden md:flex ml-10 space-x-8">
|
||||
{[
|
||||
{ id: 'inicio', label: 'Inicio' },
|
||||
{ id: 'caracteristicas', label: 'Características' },
|
||||
{ id: 'estadisticas', label: 'Confianza' },
|
||||
{ id: 'testimonios', label: 'Testimonios' },
|
||||
{ id: 'precios', label: 'Precios' },
|
||||
{ id: 'contacto', label: 'Contacto' }
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => scrollToSection(item.id)}
|
||||
className={`text-sm font-medium transition-all duration-200 hover:scale-105 ${
|
||||
activeSection === item.id
|
||||
? 'text-indigo-600 border-b-2 border-indigo-600'
|
||||
: isScrolled
|
||||
? 'text-gray-700 hover:text-indigo-600'
|
||||
: 'text-white hover:text-indigo-200'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
to="/login"
|
||||
className="bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white px-6 py-2 rounded-full text-sm font-medium transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
||||
>
|
||||
Acceder
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section con efectos */}
|
||||
<section id="inicio" className="relative min-h-screen flex items-center justify-center bg-gradient-to-br from-indigo-900 via-blue-800 to-purple-900 overflow-hidden">
|
||||
{/* Efectos de fondo animados */}
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute top-1/4 left-1/4 w-72 h-72 bg-indigo-500 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"></div>
|
||||
<div className="absolute top-1/3 right-1/4 w-96 h-96 bg-purple-500 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse animation-delay-2000"></div>
|
||||
<div className="absolute bottom-1/4 left-1/3 w-80 h-80 bg-blue-500 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse animation-delay-4000"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24 text-center">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-5xl sm:text-6xl md:text-7xl font-extrabold text-white mb-6 leading-tight">
|
||||
<span className="bg-gradient-to-r from-yellow-400 to-orange-500 bg-clip-text text-transparent animate-pulse">
|
||||
EFC
|
||||
</span>
|
||||
<br />
|
||||
<span className="text-3xl sm:text-4xl md:text-5xl bg-gradient-to-r from-blue-200 to-purple-200 bg-clip-text text-transparent">
|
||||
Para Agentes Aduanales e Importadores
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xl sm:text-2xl text-blue-100 mb-12 max-w-4xl mx-auto leading-relaxed">
|
||||
La plataforma líder para agentes aduanales e importadores que buscan
|
||||
<span className="font-semibold text-yellow-300"> digitalizar y revolucionar</span>
|
||||
{' '}sus procesos de comercio exterior. Gestiona pedimentos, documentación aduanal
|
||||
y expedientes fiscales con total seguridad y eficiencia.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-6 justify-center mb-16">
|
||||
<Link
|
||||
to="/login"
|
||||
className="group inline-flex items-center px-8 py-4 border border-transparent text-lg font-medium rounded-full text-indigo-900 bg-gradient-to-r from-yellow-400 to-orange-500 hover:from-yellow-500 hover:to-orange-600 transition-all duration-300 shadow-2xl hover:shadow-yellow-500/25 transform hover:-translate-y-1 hover:scale-105"
|
||||
>
|
||||
<span className="mr-2">🚀</span>
|
||||
Acceder a la Plataforma
|
||||
<svg className="ml-2 -mr-1 w-5 h-5 group-hover:translate-x-1 transition-transform" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => scrollToSection('contacto')}
|
||||
className="group inline-flex items-center px-8 py-4 border-2 border-white/30 text-lg font-medium rounded-full text-white bg-white/10 backdrop-blur-md hover:bg-white/20 transition-all duration-300 shadow-xl hover:shadow-white/25"
|
||||
>
|
||||
<svg className="mr-2 w-5 h-5 group-hover:rotate-12 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M19 10a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Solicitar Demo Gratuita
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Estadísticas animadas */}
|
||||
<div id="estadisticas" className="grid grid-cols-2 md:grid-cols-4 gap-8 mt-20">
|
||||
{stats.map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="text-center p-6 bg-white/10 backdrop-blur-md rounded-2xl border border-white/20 hover:bg-white/20 transition-all duration-300 transform hover:-translate-y-2"
|
||||
>
|
||||
<div className="text-4xl mb-2">{stat.icon}</div>
|
||||
<div className="text-3xl font-bold text-white mb-1">{stat.number}</div>
|
||||
<div className="text-blue-200 text-sm">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll indicator */}
|
||||
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce">
|
||||
<button
|
||||
onClick={() => scrollToSection('caracteristicas')}
|
||||
className="text-white/70 hover:text-white transition-colors"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="relative bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
{/* Features Section */}
|
||||
<div id="caracteristicas" className="mt-20">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-extrabold text-gray-900 mb-4">
|
||||
Soluciones Especializadas para Comercio Exterior
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600">
|
||||
Herramientas diseñadas específicamente para las necesidades de agentes aduanales e importadores
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{/* Feature 1 */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition duration-200">
|
||||
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg className="w-6 h-6 text-indigo-600" 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-xl font-semibold text-gray-900 mb-2">
|
||||
Gestión de Pedimentos
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Administra pedimentos de importación y exportación, documentos aduanales,
|
||||
clasificaciones arancelarias y toda la documentación requerida por el SAT.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 2 */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition duration-200">
|
||||
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Control por Organización
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Gestiona múltiples clientes importadores con espacios de trabajo separados,
|
||||
permisos granulares y control total sobre el acceso a la información.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 3 */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition duration-200">
|
||||
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
|
||||
<svg className="w-6 h-6 text-indigo-600" 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>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Reportes Aduanales
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Genera reportes especializados para auditorías, seguimiento de operaciones
|
||||
aduanales, estadísticas de importación y cumplimiento normativo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Benefits Section */}
|
||||
<div className="mt-20 bg-indigo-50 rounded-2xl p-8 md:p-12">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-extrabold text-gray-900 mb-4">
|
||||
¿Por qué elegir EFC?
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600">
|
||||
Diseñado por expertos en comercio exterior para profesionales del sector
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-indigo-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<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 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Cumplimiento</h3>
|
||||
<p className="text-gray-600 text-sm">Cumple con todas las regulaciones del SAT y normativas aduanales mexicanas</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-indigo-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<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="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Eficiencia</h3>
|
||||
<p className="text-gray-600 text-sm">Reduce hasta 70% el tiempo en gestión documental y procesos administrativos</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-indigo-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<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="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Seguridad</h3>
|
||||
<p className="text-gray-600 text-sm">Cifrado de extremo a extremo y controles de acceso empresariales</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-indigo-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<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="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M12 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Soporte</h3>
|
||||
<p className="text-gray-600 text-sm">Soporte especializado con conocimiento profundo en comercio exterior</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Testimonios Section */}
|
||||
<div id="testimonios" className="mt-20">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-extrabold text-gray-900 mb-4">
|
||||
Lo que dicen nuestros clientes
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600">
|
||||
Agentes aduanales e importadores que ya transformaron su operación con EFC
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-12 h-12 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-indigo-600 font-semibold">JM</span>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h4 className="font-semibold text-gray-900">José María González</h4>
|
||||
<p className="text-gray-600 text-sm">Agente Aduanal Patente 1234</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 italic">
|
||||
"EFC ha revolucionado nuestra operación. La gestión de pedimentos es ahora 60% más rápida
|
||||
y tenemos control total sobre todos nuestros clientes importadores."
|
||||
</p>
|
||||
<div className="flex mt-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<svg key={i} className="w-5 h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-12 h-12 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-indigo-600 font-semibold">LR</span>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h4 className="font-semibold text-gray-900">Laura Rodríguez</h4>
|
||||
<p className="text-gray-600 text-sm">Directora de Comercio Exterior</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 italic">
|
||||
"Como importador, necesitábamos una solución que nos diera visibilidad completa de nuestros procesos.
|
||||
EFC nos permite colaborar eficientemente con nuestro agente aduanal."
|
||||
</p>
|
||||
<div className="flex mt-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<svg key={i} className="w-5 h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-12 h-12 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-indigo-600 font-semibold">CT</span>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h4 className="font-semibold text-gray-900">Carlos Torres</h4>
|
||||
<p className="text-gray-600 text-sm">Agente Aduanal Patente 5678</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 italic">
|
||||
"La seguridad y el cumplimiento normativo de EFC nos dan tranquilidad total.
|
||||
Nuestros clientes valoran mucho la transparencia que ofrecemos ahora."
|
||||
</p>
|
||||
<div className="flex mt-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<svg key={i} className="w-5 h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Precios Section */}
|
||||
<div id="precios" className="mt-20">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-extrabold text-gray-900 mb-4">
|
||||
Planes diseñados para tu crecimiento
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600">
|
||||
Desde agencias pequeñas hasta grandes corporaciones
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{/* Plan Básico */}
|
||||
<div className="bg-white rounded-lg shadow-md p-8 border border-gray-200">
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-semibold text-gray-900 mb-2">Básico</h3>
|
||||
<p className="text-gray-600 mb-4">Para agencias pequeñas</p>
|
||||
<div className="mb-6">
|
||||
<span className="text-4xl font-bold text-gray-900">$2,999</span>
|
||||
<span className="text-gray-600">/mes</span>
|
||||
</div>
|
||||
<ul className="text-left space-y-3 mb-8">
|
||||
<li className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Hasta 5 usuarios
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
50GB almacenamiento
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Gestión de pedimentos
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Soporte por email
|
||||
</li>
|
||||
</ul>
|
||||
<button className="w-full bg-indigo-600 text-white py-3 px-4 rounded-md hover:bg-indigo-700 transition duration-200">
|
||||
Comenzar prueba gratuita
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan Profesional */}
|
||||
<div className="bg-white rounded-lg shadow-xl p-8 border-2 border-indigo-500 relative">
|
||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||
<span className="bg-indigo-500 text-white px-4 py-1 rounded-full text-sm font-medium">
|
||||
Más Popular
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-semibold text-gray-900 mb-2">Profesional</h3>
|
||||
<p className="text-gray-600 mb-4">Para agencias en crecimiento</p>
|
||||
<div className="mb-6">
|
||||
<span className="text-4xl font-bold text-gray-900">$5,999</span>
|
||||
<span className="text-gray-600">/mes</span>
|
||||
</div>
|
||||
<ul className="text-left space-y-3 mb-8">
|
||||
<li className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Hasta 25 usuarios
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
200GB almacenamiento
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Todas las funciones básicas
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Reportes avanzados
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Soporte prioritario
|
||||
</li>
|
||||
</ul>
|
||||
<button className="w-full bg-indigo-600 text-white py-3 px-4 rounded-md hover:bg-indigo-700 transition duration-200">
|
||||
Comenzar prueba gratuita
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan Empresarial */}
|
||||
<div className="bg-white rounded-lg shadow-md p-8 border border-gray-200">
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-semibold text-gray-900 mb-2">Empresarial</h3>
|
||||
<p className="text-gray-600 mb-4">Para grandes corporaciones</p>
|
||||
<div className="mb-6">
|
||||
<span className="text-4xl font-bold text-gray-900">$12,999</span>
|
||||
<span className="text-gray-600">/mes</span>
|
||||
</div>
|
||||
<ul className="text-left space-y-3 mb-8">
|
||||
<li className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Usuarios ilimitados
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
1TB almacenamiento
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Todas las funciones
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
API personalizada
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Soporte 24/7
|
||||
</li>
|
||||
</ul>
|
||||
<button className="w-full bg-indigo-600 text-white py-3 px-4 rounded-md hover:bg-indigo-700 transition duration-200">
|
||||
Contactar ventas
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contacto Section */}
|
||||
<div id="contacto" className="mt-20 bg-white rounded-2xl shadow-lg p-8 md:p-12">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-gray-900 mb-6">
|
||||
¿Listo para transformar tu operación?
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 mb-8">
|
||||
Nuestro equipo de expertos en comercio exterior está aquí para ayudarte.
|
||||
Contáctanos y descubre cómo EFC puede optimizar tus procesos aduanales.
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center">
|
||||
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Teléfono</h3>
|
||||
<p className="text-gray-600">+52 (55) 1234-5678</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Email</h3>
|
||||
<p className="text-gray-600">contacto@efc.com.mx</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Oficina</h3>
|
||||
<p className="text-gray-600">Ciudad de México, México</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form onSubmit={handleContactSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nombre completo *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
required
|
||||
value={contactForm.name}
|
||||
onChange={(e) => setContactForm({...contactForm, name: e.target.value})}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
placeholder="Tu nombre completo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email corporativo *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
required
|
||||
value={contactForm.email}
|
||||
onChange={(e) => setContactForm({...contactForm, email: e.target.value})}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
placeholder="tu.email@empresa.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="company" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Empresa / Agencia Aduanal
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="company"
|
||||
value={contactForm.company}
|
||||
onChange={(e) => setContactForm({...contactForm, company: e.target.value})}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
placeholder="Nombre de tu empresa"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Mensaje *
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
rows={4}
|
||||
required
|
||||
value={contactForm.message}
|
||||
onChange={(e) => setContactForm({...contactForm, message: e.target.value})}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
placeholder="Cuéntanos sobre tus necesidades..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-indigo-600 text-white py-3 px-6 rounded-md hover:bg-indigo-700 transition duration-200 font-medium"
|
||||
>
|
||||
Enviar mensaje
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-20 bg-gradient-to-r from-indigo-600 to-purple-600 rounded-2xl p-8 md:p-12 text-center text-white">
|
||||
<h2 className="text-3xl font-extrabold mb-4">
|
||||
¿Listo para digitalizar tu operación aduanal?
|
||||
</h2>
|
||||
<p className="text-xl mb-8 opacity-90">
|
||||
Únete a los agentes aduanales e importadores que ya confían en EFC
|
||||
para gestionar sus procesos de comercio exterior de manera eficiente y segura.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link
|
||||
to="/login"
|
||||
className="inline-flex items-center px-8 py-4 border border-white text-lg font-medium rounded-md text-indigo-600 bg-white hover:bg-gray-50 transition duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
||||
>
|
||||
Comenzar Ahora
|
||||
<svg className="ml-2 -mr-1 w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</Link>
|
||||
<button className="inline-flex items-center px-8 py-4 border-2 border-white text-lg font-medium rounded-md text-white bg-transparent hover:bg-white hover:text-indigo-600 transition duration-200">
|
||||
<svg className="mr-2 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
Contactar Ventas
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Call to Action Section */}
|
||||
<div className="mt-20 bg-gradient-to-r from-indigo-600 to-purple-600 rounded-2xl p-8 md:p-12 text-center text-white">
|
||||
<h2 className="text-3xl font-extrabold mb-4">
|
||||
¿Listo para digitalizar tu operación aduanal?
|
||||
</h2>
|
||||
<p className="text-xl mb-8 opacity-90">
|
||||
Únete a los agentes aduanales e importadores que ya confían en EFC
|
||||
para gestionar sus procesos de comercio exterior de manera eficiente y segura.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link
|
||||
to="/login"
|
||||
className="inline-flex items-center px-8 py-4 border border-white text-lg font-medium rounded-md text-indigo-600 bg-white hover:bg-gray-50 transition duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
||||
>
|
||||
Comenzar Ahora
|
||||
<svg className="ml-2 -mr-1 w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</Link>
|
||||
<a
|
||||
href="#contacto"
|
||||
className="inline-flex items-center px-8 py-4 border-2 border-white text-lg font-medium rounded-md text-white bg-transparent hover:bg-white hover:text-indigo-600 transition duration-200"
|
||||
>
|
||||
<svg className="mr-2 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
Contactar Ventas
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-gray-900 text-white mt-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div className="col-span-1 md:col-span-2">
|
||||
<h3 className="text-2xl font-bold mb-4">
|
||||
<span className="text-indigo-400">EFC</span>
|
||||
</h3>
|
||||
<p className="text-gray-300 mb-4">
|
||||
La plataforma líder para agentes aduanales e importadores.
|
||||
Digitaliza y optimiza tus procesos de comercio exterior con total seguridad.
|
||||
</p>
|
||||
<p className="text-gray-400 text-sm">
|
||||
Desarrollado con ❤️ por <span className="text-indigo-400 font-semibold">@Aduanasoft</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold mb-4">Producto</h4>
|
||||
<ul className="space-y-2">
|
||||
<li><a href="#" className="text-gray-300 hover:text-white transition duration-200">Características</a></li>
|
||||
<li><a href="#" className="text-gray-300 hover:text-white transition duration-200">Precios</a></li>
|
||||
<li><a href="#" className="text-gray-300 hover:text-white transition duration-200">Seguridad</a></li>
|
||||
<li><a href="#" className="text-gray-300 hover:text-white transition duration-200">API</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold mb-4">Soporte</h4>
|
||||
<ul className="space-y-2">
|
||||
<li><a href="#contacto" className="text-gray-300 hover:text-white transition duration-200">Contacto</a></li>
|
||||
<li><a href="#" className="text-gray-300 hover:text-white transition duration-200">Documentación</a></li>
|
||||
<li><a href="#" className="text-gray-300 hover:text-white transition duration-200">Centro de ayuda</a></li>
|
||||
<li><a href="#" className="text-gray-300 hover:text-white transition duration-200">Estado del servicio</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-800 mt-8 pt-8 flex flex-col md:flex-row justify-between items-center">
|
||||
<p className="text-gray-400 text-sm">
|
||||
© 2025 EFC by @Aduanasoft. Todos los derechos reservados.
|
||||
</p>
|
||||
<div className="flex space-x-6 mt-4 md:mt-0">
|
||||
<a href="#" className="text-gray-400 hover:text-white transition duration-200">
|
||||
<span className="sr-only">Twitter</span>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" className="text-gray-400 hover:text-white transition duration-200">
|
||||
<span className="sr-only">LinkedIn</span>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" className="text-gray-400 hover:text-white transition duration-200">
|
||||
<span className="sr-only">GitHub</span>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
998
src/pages/LandingAnimated.jsx
Normal file
998
src/pages/LandingAnimated.jsx
Normal file
@@ -0,0 +1,998 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { colors, tailwindClasses } from '../theme';
|
||||
|
||||
export default function LandingAnimated() {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [activeSection, setActiveSection] = useState('inicio');
|
||||
const [visibleElements, setVisibleElements] = useState(new Set());
|
||||
const [contactForm, setContactForm] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
company: '',
|
||||
message: ''
|
||||
});
|
||||
|
||||
const observerRef = useRef(null);
|
||||
const sectionsRef = useRef({});
|
||||
|
||||
// Configurar Intersection Observer para animaciones y navegación activa
|
||||
useEffect(() => {
|
||||
// Observer para animaciones de elementos
|
||||
const animationObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const elementId = entry.target.dataset.animate;
|
||||
if (elementId) {
|
||||
setVisibleElements(prev => new Set([...prev, elementId]));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -100px 0px'
|
||||
}
|
||||
);
|
||||
|
||||
// Observer para navegación activa
|
||||
const navigationObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveSection(entry.target.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: 0.3,
|
||||
rootMargin: '-20% 0px -70% 0px'
|
||||
}
|
||||
);
|
||||
|
||||
// Observar elementos para animaciones
|
||||
const animatedElements = document.querySelectorAll('[data-animate]');
|
||||
animatedElements.forEach(el => animationObserver.observe(el));
|
||||
|
||||
// Observar secciones para navegación
|
||||
const sections = document.querySelectorAll('section[id]');
|
||||
sections.forEach(section => navigationObserver.observe(section));
|
||||
|
||||
return () => {
|
||||
animationObserver.disconnect();
|
||||
navigationObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Efecto de scroll para navbar
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 50);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
// Smooth scroll para navegación
|
||||
const scrollToSection = (sectionId) => {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleContactSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
alert('Gracias por tu mensaje. Nos pondremos en contacto contigo pronto.');
|
||||
setContactForm({ name: '', email: '', company: '', message: '' });
|
||||
};
|
||||
|
||||
// Clase de animación condicional
|
||||
const getAnimationClass = (elementId, baseClass = '') => {
|
||||
return visibleElements.has(elementId)
|
||||
? `${baseClass} animate-fade-in-up opacity-100 translate-y-0`
|
||||
: `${baseClass} opacity-0 translate-y-8`;
|
||||
};
|
||||
|
||||
// Estadísticas animadas
|
||||
const stats = [
|
||||
{ number: '500+', label: 'Agentes Aduanales', icon: '🏢' },
|
||||
{ number: '15,000+', label: 'Pedimentos Procesados', icon: '📋' },
|
||||
{ number: '99.9%', label: 'Uptime Garantizado', icon: '⚡' },
|
||||
{ number: '24/7', label: 'Soporte Especializado', icon: '🛡️' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen" style={{ backgroundColor: '#F2F4F7' }}>
|
||||
{/* Navbar flotante con efectos */}
|
||||
<header className={`fixed top-0 w-full z-50 transition-all duration-300 ${
|
||||
isScrolled
|
||||
? 'bg-white/95 backdrop-blur-md shadow-lg border-b border-gray-200'
|
||||
: 'bg-transparent'
|
||||
}`}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center py-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<h1 className="text-2xl font-bold">
|
||||
<span
|
||||
className="bg-clip-text text-transparent"
|
||||
style={{
|
||||
background: `linear-gradient(to right, #1B2A41, #4DA6FF)`,
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent'
|
||||
}}
|
||||
>
|
||||
EFC
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<nav className="hidden md:flex ml-10 space-x-8">
|
||||
{[
|
||||
{ id: 'inicio', label: 'Inicio' },
|
||||
{ id: 'estadisticas', label: 'Confianza' },
|
||||
{ id: 'caracteristicas', label: 'Características' },
|
||||
{ id: 'testimonios', label: 'Testimonios' },
|
||||
{ id: 'precios', label: 'Precios' },
|
||||
{ id: 'contacto', label: 'Contacto' }
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => scrollToSection(item.id)}
|
||||
className={`relative text-sm font-medium transition-all duration-300 hover:scale-105 group`}
|
||||
style={{
|
||||
color: activeSection === item.id
|
||||
? '#1B2A41'
|
||||
: isScrolled
|
||||
? '#333333'
|
||||
: 'white'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (activeSection !== item.id) {
|
||||
e.target.style.color = isScrolled ? '#1B2A41' : '#4DA6FF';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (activeSection !== item.id) {
|
||||
e.target.style.color = isScrolled ? '#333333' : 'white';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
<span
|
||||
className={`absolute -bottom-1 left-0 h-0.5 transition-all duration-300 ${
|
||||
activeSection === item.id ? 'w-full' : 'w-0 group-hover:w-full'
|
||||
}`}
|
||||
style={{ backgroundColor: '#1B2A41' }}
|
||||
></span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
to="/login"
|
||||
className="px-6 py-2 rounded-full text-sm font-medium transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 text-white"
|
||||
style={{
|
||||
background: 'linear-gradient(to right, #1B2A41, #4DA6FF)',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.background = 'linear-gradient(to right, #162234, #1976D2)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = 'linear-gradient(to right, #1B2A41, #4DA6FF)';
|
||||
}}
|
||||
>
|
||||
Acceder
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section con efectos de gradiente animado */}
|
||||
<section id="inicio" className="relative min-h-screen flex items-center overflow-hidden">
|
||||
{/* Background con gradientes animados */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #1B2A41 0%, #263549 50%, #1976D2 100%)'
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-32 text-center">
|
||||
<div className="space-y-8">
|
||||
<h1
|
||||
data-animate="hero-title"
|
||||
className={`text-5xl sm:text-6xl md:text-7xl font-extrabold text-white transition-all duration-1000 ${
|
||||
visibleElements.has('hero-title')
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-10'
|
||||
}`}
|
||||
>
|
||||
<span className="block">
|
||||
<span
|
||||
className="bg-clip-text text-transparent"
|
||||
style={{
|
||||
background: 'linear-gradient(to right, white, #64B5F6)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent'
|
||||
}}
|
||||
>
|
||||
EFC
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="block text-3xl sm:text-4xl md:text-5xl mt-4 bg-clip-text text-transparent"
|
||||
style={{
|
||||
background: 'linear-gradient(to right, #64B5F6, white)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent'
|
||||
}}
|
||||
>
|
||||
Para Agentes Aduanales
|
||||
</span>
|
||||
<span
|
||||
className="block text-3xl sm:text-4xl md:text-5xl bg-clip-text text-transparent"
|
||||
style={{
|
||||
background: 'linear-gradient(to right, white, #64B5F6)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent'
|
||||
}}
|
||||
>
|
||||
e Importadores
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p
|
||||
data-animate="hero-subtitle"
|
||||
className={`text-xl sm:text-2xl max-w-4xl mx-auto leading-relaxed transition-all duration-1000 delay-300 ${
|
||||
visibleElements.has('hero-subtitle')
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-10'
|
||||
}`}
|
||||
style={{ color: '#64B5F6' }}
|
||||
>
|
||||
La plataforma líder desarrollada por
|
||||
<span className="font-bold text-white"> @AduanaSoft</span> para
|
||||
<span className="font-semibold" style={{ color: '#FF9800' }}> digitalizar y optimizar</span>
|
||||
{' '}todos tus procesos de comercio exterior con tecnología de vanguardia
|
||||
</p>
|
||||
|
||||
<div
|
||||
data-animate="hero-buttons"
|
||||
className={`flex flex-col sm:flex-row gap-6 justify-center transition-all duration-1000 delay-500 ${
|
||||
visibleElements.has('hero-buttons')
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-10'
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
to="/login"
|
||||
className="group inline-flex items-center px-8 py-4 text-lg font-semibold rounded-full transition-all duration-300 shadow-2xl hover:shadow-3xl transform hover:-translate-y-1 hover:scale-105"
|
||||
style={{
|
||||
color: '#1B2A41',
|
||||
background: 'linear-gradient(to right, white, #F2F4F7)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.background = 'linear-gradient(to right, #F2F4F7, white)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = 'linear-gradient(to right, white, #F2F4F7)';
|
||||
}}
|
||||
>
|
||||
<span>Comenzar Ahora</span>
|
||||
<svg className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => scrollToSection('caracteristicas')}
|
||||
className="group inline-flex items-center px-8 py-4 text-lg font-semibold rounded-full text-white bg-transparent border-2 border-white/30 hover:border-white hover:bg-white/10 transition-all duration-300 backdrop-blur-sm"
|
||||
>
|
||||
<svg className="mr-2 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M19 10a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>Ver Demo</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Floating cards con efectos */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-4xl mx-auto">
|
||||
{[
|
||||
{ icon: '🚀', title: 'Rápido', desc: 'Procesamiento instantáneo' },
|
||||
{ icon: '🔒', title: 'Seguro', desc: 'Cifrado de nivel bancario' },
|
||||
{ icon: '📊', title: 'Inteligente', desc: 'IA para optimización' }
|
||||
].map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
data-animate={`hero-card-${index}`}
|
||||
className={`bg-white/10 backdrop-blur-md rounded-2xl p-6 border border-white/20 hover:bg-white/20 transition-all duration-500 hover:scale-105 hover:shadow-2xl ${
|
||||
visibleElements.has(`hero-card-${index}`)
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-10'
|
||||
}`}
|
||||
style={{ transitionDelay: `${700 + index * 200}ms` }}
|
||||
>
|
||||
<div className="text-4xl mb-3">{feature.icon}</div>
|
||||
<h3 className="text-white font-semibold text-lg mb-2">{feature.title}</h3>
|
||||
<p className="text-sm" style={{ color: '#64B5F6' }}>{feature.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll indicator animado */}
|
||||
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce">
|
||||
<button
|
||||
onClick={() => scrollToSection('estadisticas')}
|
||||
className="text-white/70 hover:text-white transition-colors duration-200"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sección de Estadísticas y Confianza */}
|
||||
<section id="estadisticas" className="py-20" style={{ background: 'linear-gradient(to right, #F2F4F7, white)' }}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div
|
||||
data-animate="stats-header"
|
||||
className={`text-center mb-16 transition-all duration-1000 ${
|
||||
visibleElements.has('stats-header')
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-10'
|
||||
}`}
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold mb-4" style={{ color: '#333333' }}>
|
||||
Más de <span style={{ color: '#1B2A41' }}>500 empresas</span> confían en nosotros
|
||||
</h2>
|
||||
<p className="text-xl max-w-3xl mx-auto" style={{ color: '#7A7A7A' }}>
|
||||
Desarrollado por <span className="font-bold" style={{ color: '#1B2A41' }}>@AduanaSoft</span>,
|
||||
líderes en tecnología aduanal con más de 10 años de experiencia
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats con animaciones */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
|
||||
{stats.map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
data-animate={`stat-${index}`}
|
||||
className={`text-center p-6 bg-white rounded-2xl shadow-lg hover:shadow-xl transition-all duration-700 hover:scale-105 border border-gray-100 ${
|
||||
visibleElements.has(`stat-${index}`)
|
||||
? 'opacity-100 translate-y-0 scale-100'
|
||||
: 'opacity-0 translate-y-10 scale-95'
|
||||
}`}
|
||||
style={{ transitionDelay: `${index * 200}ms` }}
|
||||
>
|
||||
<div className="text-4xl mb-4 animate-pulse">{stat.icon}</div>
|
||||
<div className="text-3xl font-bold mb-2" style={{ color: '#1B2A41' }}>{stat.number}</div>
|
||||
<div className="font-medium" style={{ color: '#7A7A7A' }}>{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* AduanaSoft Info */}
|
||||
<div
|
||||
data-animate="aduanasoft-info"
|
||||
className={`rounded-3xl p-8 md:p-12 text-white transition-all duration-1000 ${
|
||||
visibleElements.has('aduanasoft-info')
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-10'
|
||||
}`}
|
||||
style={{ background: 'linear-gradient(to right, #1B2A41, #263549)' }}
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-8 items-center">
|
||||
<div>
|
||||
<h3 className="text-3xl font-bold mb-6">Acerca de AduanaSoft</h3>
|
||||
<div className="space-y-4 text-indigo-100">
|
||||
{[
|
||||
"10+ años especializados en software aduanal",
|
||||
"Equipo experto en comercio exterior y tecnología",
|
||||
"Certificación SAT y cumplimiento normativo total",
|
||||
"Soporte 24/7 con especialistas aduanales"
|
||||
].map((item, idx) => (
|
||||
<div key={idx} className="flex items-start space-x-3">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-white/20 rounded-full flex items-center justify-center mt-1">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<p><strong>{item.split(' ')[0]} {item.split(' ')[1]}</strong> {item.split(' ').slice(2).join(' ')}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-8 border border-white/20">
|
||||
<div className="text-6xl mb-4">🏆</div>
|
||||
<h4 className="text-2xl font-bold mb-2">Líder del Mercado</h4>
|
||||
<p className="text-indigo-100">
|
||||
Reconocidos como la mejor solución tecnológica para agentes aduanales en México
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Características con efectos interactivos */}
|
||||
<section id="caracteristicas" className="py-20 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div
|
||||
data-animate="features-header"
|
||||
className={`text-center mb-16 transition-all duration-1000 ${
|
||||
visibleElements.has('features-header')
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-10'
|
||||
}`}
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-gray-900 mb-4">
|
||||
Soluciones Especializadas para Comercio Exterior
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
Herramientas diseñadas específicamente para las necesidades de agentes aduanales e importadores
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
icon: '📋',
|
||||
title: 'Gestión de Pedimentos',
|
||||
description: 'Administra pedimentos de importación y exportación, documentos aduanales, clasificaciones arancelarias y toda la documentación requerida por el SAT.',
|
||||
features: ['Validación automática SAT', 'Clasificación arancelaria', 'Cálculo de impuestos', 'Trazabilidad completa']
|
||||
},
|
||||
{
|
||||
icon: '🏢',
|
||||
title: 'Control por Organización',
|
||||
description: 'Gestiona múltiples clientes importadores con espacios de trabajo separados, permisos granulares y control total sobre el acceso a la información.',
|
||||
features: ['Multi-tenancy', 'Roles y permisos', 'Auditoría completa', 'Segregación de datos']
|
||||
},
|
||||
{
|
||||
icon: '📊',
|
||||
title: 'Reportes Aduanales',
|
||||
description: 'Genera reportes especializados para auditorías, seguimiento de operaciones aduanales, estadísticas de importación y cumplimiento normativo.',
|
||||
features: ['Dashboards en tiempo real', 'Exportación múltiple', 'KPIs personalizados', 'Alertas automáticas']
|
||||
}
|
||||
].map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
data-animate={`feature-${index}`}
|
||||
className={`group bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-700 overflow-hidden border border-gray-100 hover:scale-105 ${
|
||||
visibleElements.has(`feature-${index}`)
|
||||
? 'opacity-100 translate-y-0 rotate-0'
|
||||
: 'opacity-0 translate-y-10 rotate-3'
|
||||
}`}
|
||||
style={{
|
||||
transitionDelay: `${index * 300}ms`,
|
||||
borderColor: visibleElements.has(`feature-${index}`) ? '#4DA6FF' : '#e5e7eb'
|
||||
}}
|
||||
>
|
||||
<div className="p-8">
|
||||
<div className={`text-5xl mb-6 transition-all duration-500 ${
|
||||
visibleElements.has(`feature-${index}`)
|
||||
? 'transform scale-100 rotate-0'
|
||||
: 'transform scale-75 rotate-12'
|
||||
}`}>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold mb-4 transition-colors duration-300" style={{
|
||||
color: visibleElements.has(`feature-${index}`) ? '#1B2A41' : '#333333'
|
||||
}}>
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="mb-6 leading-relaxed" style={{ color: '#7A7A7A' }}>
|
||||
{feature.description}
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
{feature.features.map((item, idx) => (
|
||||
<li key={idx} className="flex items-center text-sm" style={{ color: '#7A7A7A' }}>
|
||||
<svg className="w-4 h-4 mr-2 flex-shrink-0" style={{ color: '#2E7D32' }} fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-4 transition-colors duration-300" style={{
|
||||
background: 'linear-gradient(to right, #F2F4F7, #FFFFFF)'
|
||||
}}>
|
||||
<button className="font-semibold text-sm transition-colors duration-200" style={{
|
||||
color: '#1B2A41'
|
||||
}}>
|
||||
Conocer más →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Testimonios */}
|
||||
<section id="testimonios" className="py-20" style={{ background: 'linear-gradient(135deg, #F2F4F7, #FFFFFF)' }}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div
|
||||
data-animate="testimonials-header"
|
||||
className={`text-center mb-16 transition-all duration-1000 ${
|
||||
visibleElements.has('testimonials-header')
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-10'
|
||||
}`}
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold mb-4" style={{ color: '#333333' }}>
|
||||
Lo que dicen nuestros clientes
|
||||
</h2>
|
||||
<p className="text-xl" style={{ color: '#7A7A7A' }}>
|
||||
Testimonios reales de agentes aduanales que han transformado su operación
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
name: 'Carlos Mendoza',
|
||||
company: 'Agente Aduanal 1234',
|
||||
image: '👨💼',
|
||||
testimonial: 'EFC revolucionó nuestra operación. Reducimos 70% el tiempo en procesar pedimentos y eliminamos errores manuales.',
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: 'María González',
|
||||
company: 'Importadora Global SA',
|
||||
image: '👩💼',
|
||||
testimonial: 'La plataforma más completa del mercado. El soporte de AduanaSoft es excepcional, entienden perfectamente nuestras necesidades.',
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: 'Roberto Silva',
|
||||
company: 'Comercio Exterior RSC',
|
||||
image: '👨💻',
|
||||
testimonial: 'Migramos de sistemas obsoletos a EFC y fue la mejor decisión. Ahora somos más eficientes y competitivos.',
|
||||
rating: 5
|
||||
}
|
||||
].map((testimonial, index) => (
|
||||
<div
|
||||
key={index}
|
||||
data-animate={`testimonial-${index}`}
|
||||
className={`bg-white rounded-2xl shadow-lg hover:shadow-xl transition-all duration-700 p-8 border border-gray-100 hover:scale-105 ${
|
||||
visibleElements.has(`testimonial-${index}`)
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-10'
|
||||
}`}
|
||||
style={{ transitionDelay: `${index * 200}ms` }}
|
||||
>
|
||||
<div className="flex items-center mb-6">
|
||||
<div className="text-4xl mr-4">{testimonial.image}</div>
|
||||
<div>
|
||||
<h4 className="font-bold" style={{ color: '#333333' }}>{testimonial.name}</h4>
|
||||
<p className="text-sm" style={{ color: '#7A7A7A' }}>{testimonial.company}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-4 italic" style={{ color: '#333333' }}>"{testimonial.testimonial}"</p>
|
||||
<div className="flex" style={{ color: '#F57C00' }}>
|
||||
{[...Array(testimonial.rating)].map((_, i) => (
|
||||
<svg key={i} className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Precios */}
|
||||
<section id="precios" className="py-20 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div
|
||||
data-animate="pricing-header"
|
||||
className={`text-center mb-16 transition-all duration-1000 ${
|
||||
visibleElements.has('pricing-header')
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-10'
|
||||
}`}
|
||||
>
|
||||
<h2 className="text-4xl font-extrabold text-gray-900 mb-4">
|
||||
Planes diseñados para tu crecimiento
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
Desde agentes independientes hasta grandes corporativos
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
name: 'Starter',
|
||||
price: '$2,999',
|
||||
period: '/mes',
|
||||
description: 'Perfecto para agentes aduanales independientes',
|
||||
features: [
|
||||
'Hasta 50 pedimentos/mes',
|
||||
'5 GB almacenamiento',
|
||||
'Soporte por email',
|
||||
'Reportes básicos',
|
||||
'2 usuarios'
|
||||
],
|
||||
popular: false
|
||||
},
|
||||
{
|
||||
name: 'Professional',
|
||||
price: '$5,999',
|
||||
period: '/mes',
|
||||
description: 'Ideal para agencias medianas',
|
||||
features: [
|
||||
'Hasta 200 pedimentos/mes',
|
||||
'20 GB almacenamiento',
|
||||
'Soporte prioritario',
|
||||
'Reportes avanzados',
|
||||
'10 usuarios',
|
||||
'API acceso',
|
||||
'Integraciones SAT'
|
||||
],
|
||||
popular: true
|
||||
},
|
||||
{
|
||||
name: 'Enterprise',
|
||||
price: 'Personalizado',
|
||||
period: '',
|
||||
description: 'Para grandes corporativos',
|
||||
features: [
|
||||
'Pedimentos ilimitados',
|
||||
'Almacenamiento ilimitado',
|
||||
'Soporte 24/7 dedicado',
|
||||
'Reportes personalizados',
|
||||
'Usuarios ilimitados',
|
||||
'API completa',
|
||||
'Implementación dedicada',
|
||||
'SLA garantizado'
|
||||
],
|
||||
popular: false
|
||||
}
|
||||
].map((plan, index) => (
|
||||
<div
|
||||
key={index}
|
||||
data-animate={`plan-${index}`}
|
||||
className={`relative rounded-2xl border-2 p-8 transition-all duration-700 hover:scale-105 ${
|
||||
plan.popular
|
||||
? 'shadow-xl'
|
||||
: 'border-gray-200 bg-white shadow-lg hover:shadow-xl'
|
||||
} ${
|
||||
visibleElements.has(`plan-${index}`)
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-10'
|
||||
}`}
|
||||
style={{
|
||||
transitionDelay: `${index * 200}ms`,
|
||||
borderColor: plan.popular ? '#1B2A41' : '#e5e7eb',
|
||||
background: plan.popular ? 'linear-gradient(to bottom, #F2F4F7, white)' : 'white'
|
||||
}}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
||||
<span className="text-white px-4 py-2 rounded-full text-sm font-semibold animate-pulse" style={{
|
||||
background: 'linear-gradient(to right, #1B2A41, #263549)'
|
||||
}}>
|
||||
Más Popular
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<h3 className="text-2xl font-bold mb-2" style={{ color: '#333333' }}>{plan.name}</h3>
|
||||
<p className="mb-4" style={{ color: '#7A7A7A' }}>{plan.description}</p>
|
||||
<div className="mb-4">
|
||||
<span className="text-4xl font-extrabold" style={{ color: '#1B2A41' }}>{plan.price}</span>
|
||||
<span style={{ color: '#7A7A7A' }}>{plan.period}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-3 mb-8">
|
||||
{plan.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="text-gray-700">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection('contacto')}
|
||||
className={`w-full py-3 px-6 rounded-full font-semibold transition-all duration-200 ${
|
||||
plan.popular
|
||||
? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white hover:from-indigo-700 hover:to-purple-700 shadow-lg hover:shadow-xl'
|
||||
: 'bg-gray-100 text-gray-900 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{plan.name === 'Enterprise' ? 'Contactar Ventas' : 'Comenzar Prueba'}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contacto */}
|
||||
<section id="contacto" className="relative py-20 overflow-hidden" style={{
|
||||
background: 'linear-gradient(135deg, #1B2A41 0%, #263549 50%, #1B2A41 100%)'
|
||||
}}>
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute inset-0" style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fillRule='evenodd'%3E%3Cg fill='%234DA6FF' fillOpacity='0.3'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`
|
||||
}}></div>
|
||||
</div>
|
||||
|
||||
{/* Floating elements */}
|
||||
<div className="absolute top-10 left-10 w-20 h-20 rounded-full opacity-20" style={{ backgroundColor: '#4DA6FF' }}></div>
|
||||
<div className="absolute bottom-10 right-10 w-32 h-32 rounded-full opacity-15" style={{ backgroundColor: '#F57C00' }}></div>
|
||||
<div className="absolute top-1/2 left-1/4 w-16 h-16 rounded-full opacity-10" style={{ backgroundColor: '#2E7D32' }}></div>
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl md:text-5xl font-extrabold text-white mb-6">
|
||||
¿Listo para <span style={{ color: '#4DA6FF' }}>transformar</span> tu operación aduanal?
|
||||
</h2>
|
||||
<p className="text-xl md:text-2xl max-w-3xl mx-auto" style={{ color: '#64B5F6' }}>
|
||||
Únete a más de 500 empresas que ya optimizaron sus procesos con EFC
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
|
||||
<div
|
||||
data-animate="contact-info"
|
||||
className={`transition-all duration-1000 ${
|
||||
visibleElements.has('contact-info')
|
||||
? 'opacity-100 translate-x-0'
|
||||
: 'opacity-0 -translate-x-10'
|
||||
}`}
|
||||
>
|
||||
{/* Card de información de contacto */}
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-3xl p-8 border border-white/20">
|
||||
<div className="mb-8">
|
||||
<h3 className="text-2xl font-bold text-white mb-4">
|
||||
Hablemos de tu proyecto
|
||||
</h3>
|
||||
<p className="text-lg" style={{ color: '#64B5F6' }}>
|
||||
Nuestros expertos en comercio exterior están listos para ayudarte
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{
|
||||
icon: 'M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z',
|
||||
title: 'Teléfono',
|
||||
info: '+52 (55) 1234-5678',
|
||||
subtitle: 'Lun - Vie, 9:00 AM - 7:00 PM'
|
||||
},
|
||||
{
|
||||
icon: 'M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
|
||||
title: 'Email',
|
||||
info: 'contacto@aduanasoft.com',
|
||||
subtitle: 'Respuesta en menos de 2 horas'
|
||||
},
|
||||
{
|
||||
icon: 'M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z M15 11a3 3 0 11-6 0 3 3 0 016 0z',
|
||||
title: 'Oficinas',
|
||||
info: 'Ciudad de México, México',
|
||||
subtitle: 'Visitas con cita previa'
|
||||
}
|
||||
].map((contact, idx) => (
|
||||
<div key={idx} className="flex items-start space-x-4 p-4 rounded-xl bg-white/5 hover:bg-white/10 transition-all duration-300">
|
||||
<div className="w-14 h-14 flex items-center justify-center rounded-full" style={{ backgroundColor: '#4DA6FF' }}>
|
||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d={contact.icon} />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-bold text-white text-lg">{contact.title}</h4>
|
||||
<p className="font-semibold mb-1" style={{ color: '#4DA6FF' }}>{contact.info}</p>
|
||||
<p className="text-sm" style={{ color: '#64B5F6' }}>{contact.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Botón adicional para WhatsApp */}
|
||||
<div className="mt-8 pt-6 border-t border-white/20">
|
||||
<a
|
||||
href="#"
|
||||
className="flex items-center justify-center w-full py-4 px-6 rounded-xl font-semibold text-white transition-all duration-300 transform hover:scale-105"
|
||||
style={{
|
||||
background: 'linear-gradient(45deg, #25D366, #128C7E)'
|
||||
}}
|
||||
>
|
||||
<svg className="w-6 h-6 mr-3" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893A11.821 11.821 0 0020.885 3.488"/>
|
||||
</svg>
|
||||
Chatear por WhatsApp
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-animate="contact-form"
|
||||
className={`bg-white rounded-2xl shadow-2xl p-8 transition-all duration-1000 ${
|
||||
visibleElements.has('contact-form')
|
||||
? 'opacity-100 translate-x-0'
|
||||
: 'opacity-0 translate-x-10'
|
||||
}`}
|
||||
>
|
||||
<h3 className="text-2xl font-bold mb-6" style={{ color: '#333333' }}>Solicita una demostración</h3>
|
||||
<form onSubmit={handleContactSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
|
||||
Nombre completo
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={contactForm.name}
|
||||
onChange={(e) => setContactForm({...contactForm, name: e.target.value})}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent transition-all duration-200"
|
||||
style={{
|
||||
focusRingColor: '#4DA6FF',
|
||||
outline: 'none'
|
||||
}}
|
||||
onFocus={(e) => e.target.style.borderColor = '#4DA6FF'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#d1d5db'}
|
||||
placeholder="Tu nombre"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
|
||||
Email corporativo
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={contactForm.email}
|
||||
onChange={(e) => setContactForm({...contactForm, email: e.target.value})}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent transition-all duration-200"
|
||||
onFocus={(e) => e.target.style.borderColor = '#4DA6FF'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#d1d5db'}
|
||||
placeholder="tu@empresa.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
|
||||
Empresa
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={contactForm.company}
|
||||
onChange={(e) => setContactForm({...contactForm, company: e.target.value})}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent transition-all duration-200"
|
||||
onFocus={(e) => e.target.style.borderColor = '#4DA6FF'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#d1d5db'}
|
||||
placeholder="Nombre de tu empresa"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
|
||||
Mensaje
|
||||
</label>
|
||||
<textarea
|
||||
rows="4"
|
||||
value={contactForm.message}
|
||||
onChange={(e) => setContactForm({...contactForm, message: e.target.value})}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent transition-all duration-200"
|
||||
onFocus={(e) => e.target.style.borderColor = '#4DA6FF'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#d1d5db'}
|
||||
placeholder="Cuéntanos sobre tu operación aduanal..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full text-white py-3 px-6 rounded-lg font-semibold transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
||||
style={{
|
||||
background: 'linear-gradient(to right, #1B2A41, #263549)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.background = 'linear-gradient(to right, #263549, #1B2A41)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = 'linear-gradient(to right, #1B2A41, #263549)';
|
||||
}}
|
||||
>
|
||||
Enviar solicitud
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="text-white py-12" style={{ backgroundColor: '#1B2A41' }}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div className="col-span-1 md:col-span-2">
|
||||
<h3 className="text-2xl font-bold mb-4">
|
||||
<span style={{
|
||||
background: 'linear-gradient(to right, #4DA6FF, #64B5F6)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent'
|
||||
}}>
|
||||
EFC
|
||||
</span>
|
||||
</h3>
|
||||
<p className="mb-4 max-w-md" style={{ color: '#7A7A7A' }}>
|
||||
La plataforma líder para agentes aduanales e importadores, desarrollada por
|
||||
<span className="font-semibold" style={{ color: '#4DA6FF' }}> @AduanaSoft</span> con más de 10 años de experiencia.
|
||||
</p>
|
||||
<div className="flex space-x-4">
|
||||
{['M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z',
|
||||
'M22.46 6c-.77.35-1.6.58-2.46.69.88-.53 1.56-1.37 1.88-2.38-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29 0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15 0 1.49.75 2.81 1.91 3.56-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07 4.28 4.28 0 0 0 4 2.98 8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21 16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56.84-.6 1.56-1.36 2.14-2.23z',
|
||||
'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z'].map((path, idx) => (
|
||||
<a key={idx} href="#" className="transition-colors duration-200" style={{ color: '#7A7A7A' }}
|
||||
onMouseEnter={(e) => e.target.style.color = 'white'}
|
||||
onMouseLeave={(e) => e.target.style.color = '#7A7A7A'}>
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d={path}/>
|
||||
</svg>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">Producto</h4>
|
||||
<ul className="space-y-2 text-gray-400">
|
||||
<li><button onClick={() => scrollToSection('caracteristicas')} className="hover:text-white transition-colors duration-200">Características</button></li>
|
||||
<li><button onClick={() => scrollToSection('precios')} className="hover:text-white transition-colors duration-200">Precios</button></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors duration-200">Integraciónes</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors duration-200">API</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">Soporte</h4>
|
||||
<ul className="space-y-2 text-gray-400">
|
||||
<li><button onClick={() => scrollToSection('contacto')} className="hover:text-white transition-colors duration-200">Contacto</button></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors duration-200">Documentación</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors duration-200">Centro de Ayuda</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors duration-200">Status</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
|
||||
<p>
|
||||
© 2025 EFC by <span className="font-semibold text-indigo-400">@AduanaSoft</span>.
|
||||
Todos los derechos reservados. | Solución especializada para Agentes Aduanales e Importadores.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
756
src/pages/LandingNew.jsx
Normal file
756
src/pages/LandingNew.jsx
Normal file
@@ -0,0 +1,756 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function Landing() {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [activeSection, setActiveSection] = useState('inicio');
|
||||
const [visibleElements, setVisibleElements] = useState(new Set());
|
||||
const [contactForm, setContactForm] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
company: '',
|
||||
message: ''
|
||||
});
|
||||
|
||||
// Efecto de scroll para navbar y detección de secciones activas
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 50);
|
||||
|
||||
// Detectar sección activa
|
||||
const sections = ['inicio', 'estadisticas', 'caracteristicas', 'testimonios', 'precios', 'contacto'];
|
||||
const currentSection = sections.find(section => {
|
||||
const element = document.getElementById(section);
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.top <= 100 && rect.bottom >= 100;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (currentSection && currentSection !== activeSection) {
|
||||
setActiveSection(currentSection);
|
||||
}
|
||||
|
||||
// Detectar elementos visibles para animaciones
|
||||
const animatedElements = document.querySelectorAll('[data-animate]');
|
||||
const newVisibleElements = new Set(visibleElements);
|
||||
|
||||
animatedElements.forEach((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
|
||||
|
||||
if (isVisible && !visibleElements.has(element.dataset.animate)) {
|
||||
newVisibleElements.add(element.dataset.animate);
|
||||
}
|
||||
});
|
||||
|
||||
if (newVisibleElements.size !== visibleElements.size) {
|
||||
setVisibleElements(newVisibleElements);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
// Trigger inicial
|
||||
handleScroll();
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [activeSection, visibleElements]);
|
||||
|
||||
// Smooth scroll para navegación
|
||||
const scrollToSection = (sectionId) => {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
setActiveSection(sectionId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContactSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
alert('Gracias por tu mensaje. Nos pondremos en contacto contigo pronto.');
|
||||
setContactForm({ name: '', email: '', company: '', message: '' });
|
||||
};
|
||||
|
||||
// Estadísticas animadas
|
||||
const stats = [
|
||||
{ number: '500+', label: 'Agentes Aduanales', icon: '🏢' },
|
||||
{ number: '15,000+', label: 'Pedimentos Procesados', icon: '📋' },
|
||||
{ number: '99.9%', label: 'Uptime Garantizado', icon: '⚡' },
|
||||
{ number: '24/7', label: 'Soporte Especializado', icon: '🛡️' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* Navbar flotante con efectos */}
|
||||
<header className={`fixed top-0 w-full z-50 transition-all duration-300 ${
|
||||
isScrolled
|
||||
? 'bg-white/95 backdrop-blur-md shadow-lg border-b border-gray-200'
|
||||
: 'bg-transparent'
|
||||
}`}>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center py-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<h1 className="text-2xl font-bold">
|
||||
<span className="bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
|
||||
EFC
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<nav className="hidden md:flex ml-10 space-x-8">
|
||||
{[
|
||||
{ id: 'inicio', label: 'Inicio' },
|
||||
{ id: 'caracteristicas', label: 'Características' },
|
||||
{ id: 'estadisticas', label: 'Confianza' },
|
||||
{ id: 'testimonios', label: 'Testimonios' },
|
||||
{ id: 'precios', label: 'Precios' },
|
||||
{ id: 'contacto', label: 'Contacto' }
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => scrollToSection(item.id)}
|
||||
className={`text-sm font-medium transition-all duration-200 hover:scale-105 ${
|
||||
activeSection === item.id
|
||||
? 'text-indigo-600 border-b-2 border-indigo-600'
|
||||
: isScrolled
|
||||
? 'text-gray-700 hover:text-indigo-600'
|
||||
: 'text-white hover:text-indigo-200'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
to="/login"
|
||||
className="bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white px-6 py-2 rounded-full text-sm font-medium transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
||||
>
|
||||
Acceder
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section con efectos de gradiente animado */}
|
||||
<section id="inicio" className="relative min-h-screen flex items-center overflow-hidden">
|
||||
{/* Background con gradientes animados */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800">
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
|
||||
<div className="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%239C92AC" fill-opacity="0.1"%3E%3Ccircle cx="30" cy="30" r="2"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E')] opacity-20"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-32 text-center">
|
||||
<div className="animate-fade-in-up">
|
||||
<h1 className="text-5xl sm:text-6xl md:text-7xl font-extrabold text-white mb-8">
|
||||
<span className="block">
|
||||
<span className="bg-gradient-to-r from-white to-indigo-200 bg-clip-text text-transparent">
|
||||
EFC
|
||||
</span>
|
||||
</span>
|
||||
<span className="block text-3xl sm:text-4xl md:text-5xl mt-4 bg-gradient-to-r from-indigo-200 to-purple-200 bg-clip-text text-transparent">
|
||||
Para Agentes Aduanales
|
||||
</span>
|
||||
<span className="block text-3xl sm:text-4xl md:text-5xl bg-gradient-to-r from-purple-200 to-pink-200 bg-clip-text text-transparent">
|
||||
e Importadores
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-xl sm:text-2xl text-indigo-100 mb-12 max-w-4xl mx-auto leading-relaxed">
|
||||
La plataforma líder desarrollada por
|
||||
<span className="font-bold text-white"> @AduanaSoft</span> para
|
||||
<span className="font-semibold text-yellow-300"> digitalizar y optimizar</span>
|
||||
{' '}todos tus procesos de comercio exterior con tecnología de vanguardia
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-6 justify-center mb-16">
|
||||
<Link
|
||||
to="/login"
|
||||
className="group inline-flex items-center px-8 py-4 text-lg font-semibold rounded-full text-indigo-900 bg-gradient-to-r from-white to-indigo-50 hover:from-indigo-50 hover:to-white transition-all duration-300 shadow-2xl hover:shadow-3xl transform hover:-translate-y-1 hover:scale-105"
|
||||
>
|
||||
<span>Comenzar Ahora</span>
|
||||
<svg className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => scrollToSection('caracteristicas')}
|
||||
className="group inline-flex items-center px-8 py-4 text-lg font-semibold rounded-full text-white bg-transparent border-2 border-white/30 hover:border-white hover:bg-white/10 transition-all duration-300 backdrop-blur-sm"
|
||||
>
|
||||
<svg className="mr-2 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M19 10a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>Ver Demo</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Floating cards con efectos */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-4xl mx-auto">
|
||||
{[
|
||||
{ icon: '🚀', title: 'Rápido', desc: 'Procesamiento instantáneo' },
|
||||
{ icon: '🔒', title: 'Seguro', desc: 'Cifrado de nivel bancario' },
|
||||
{ icon: '📊', title: 'Inteligente', desc: 'IA para optimización' }
|
||||
].map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white/10 backdrop-blur-md rounded-2xl p-6 border border-white/20 hover:bg-white/20 transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
||||
style={{ animationDelay: `${index * 0.2}s` }}
|
||||
>
|
||||
<div className="text-4xl mb-3">{feature.icon}</div>
|
||||
<h3 className="text-white font-semibold text-lg mb-2">{feature.title}</h3>
|
||||
<p className="text-indigo-200 text-sm">{feature.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll indicator animado */}
|
||||
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce">
|
||||
<button
|
||||
onClick={() => scrollToSection('estadisticas')}
|
||||
className="text-white/70 hover:text-white transition-colors duration-200"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sección de Estadísticas y Confianza */}
|
||||
<section id="estadisticas" className="py-20 bg-gradient-to-r from-gray-50 to-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl font-extrabold text-gray-900 mb-4">
|
||||
Más de <span className="text-indigo-600">500 empresas</span> confían en nosotros
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Desarrollado por <span className="font-bold text-indigo-600">@AduanaSoft</span>,
|
||||
líderes en tecnología aduanal con más de 10 años de experiencia
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats con animaciones */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 mb-16">
|
||||
{stats.map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="text-center p-6 bg-white rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 border border-gray-100"
|
||||
>
|
||||
<div className="text-4xl mb-4">{stat.icon}</div>
|
||||
<div className="text-3xl font-bold text-indigo-600 mb-2">{stat.number}</div>
|
||||
<div className="text-gray-600 font-medium">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* AduanaSoft Info */}
|
||||
<div className="bg-gradient-to-r from-indigo-600 to-purple-600 rounded-3xl p-8 md:p-12 text-white">
|
||||
<div className="grid md:grid-cols-2 gap-8 items-center">
|
||||
<div>
|
||||
<h3 className="text-3xl font-bold mb-6">Acerca de AduanaSoft</h3>
|
||||
<div className="space-y-4 text-indigo-100">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-white/20 rounded-full flex items-center justify-center mt-1">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<p><strong>10+ años</strong> especializados en software aduanal</p>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-white/20 rounded-full flex items-center justify-center mt-1">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<p><strong>Equipo experto</strong> en comercio exterior y tecnología</p>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-white/20 rounded-full flex items-center justify-center mt-1">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<p><strong>Certificación SAT</strong> y cumplimiento normativo total</p>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-white/20 rounded-full flex items-center justify-center mt-1">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<p><strong>Soporte 24/7</strong> con especialistas aduanales</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-8 border border-white/20">
|
||||
<div className="text-6xl mb-4">🏆</div>
|
||||
<h4 className="text-2xl font-bold mb-2">Líder del Mercado</h4>
|
||||
<p className="text-indigo-100">
|
||||
Reconocidos como la mejor solución tecnológica para agentes aduanales en México
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Características con efectos interactivos */}
|
||||
<section id="caracteristicas" className="py-20 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl font-extrabold text-gray-900 mb-4">
|
||||
Soluciones Especializadas para Comercio Exterior
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
Herramientas diseñadas específicamente para las necesidades de agentes aduanales e importadores
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
icon: '📋',
|
||||
title: 'Gestión de Pedimentos',
|
||||
description: 'Administra pedimentos de importación y exportación, documentos aduanales, clasificaciones arancelarias y toda la documentación requerida por el SAT.',
|
||||
features: ['Validación automática SAT', 'Clasificación arancelaria', 'Cálculo de impuestos', 'Trazabilidad completa']
|
||||
},
|
||||
{
|
||||
icon: '🏢',
|
||||
title: 'Control por Organización',
|
||||
description: 'Gestiona múltiples clientes importadores con espacios de trabajo separados, permisos granulares y control total sobre el acceso a la información.',
|
||||
features: ['Multi-tenancy', 'Roles y permisos', 'Auditoría completa', 'Segregación de datos']
|
||||
},
|
||||
{
|
||||
icon: '📊',
|
||||
title: 'Reportes Aduanales',
|
||||
description: 'Genera reportes especializados para auditorías, seguimiento de operaciones aduanales, estadísticas de importación y cumplimiento normativo.',
|
||||
features: ['Dashboards en tiempo real', 'Exportación múltiple', 'KPIs personalizados', 'Alertas automáticas']
|
||||
}
|
||||
].map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="group bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden border border-gray-100 hover:border-indigo-200 hover:scale-105"
|
||||
>
|
||||
<div className="p-8">
|
||||
<div className="text-5xl mb-6 group-hover:scale-110 transition-transform duration-300">
|
||||
{feature.icon}
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4 group-hover:text-indigo-600 transition-colors duration-300">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-6 leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
{feature.features.map((item, idx) => (
|
||||
<li key={idx} className="flex items-center text-sm text-gray-500">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-gradient-to-r from-indigo-50 to-purple-50 p-4 group-hover:from-indigo-100 group-hover:to-purple-100 transition-colors duration-300">
|
||||
<button className="text-indigo-600 font-semibold text-sm hover:text-indigo-700 transition-colors duration-200">
|
||||
Conocer más →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Testimonios */}
|
||||
<section id="testimonios" className="py-20 bg-gradient-to-br from-gray-50 to-indigo-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl font-extrabold text-gray-900 mb-4">
|
||||
Lo que dicen nuestros clientes
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
Testimonios reales de agentes aduanales que han transformado su operación
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
name: 'Carlos Mendoza',
|
||||
company: 'Agente Aduanal 1234',
|
||||
image: '👨💼',
|
||||
testimonial: 'EFC revolucionó nuestra operación. Reducimos 70% el tiempo en procesar pedimentos y eliminamos errores manuales.',
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: 'María González',
|
||||
company: 'Importadora Global SA',
|
||||
image: '👩💼',
|
||||
testimonial: 'La plataforma más completa del mercado. El soporte de AduanaSoft es excepcional, entienden perfectamente nuestras necesidades.',
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: 'Roberto Silva',
|
||||
company: 'Comercio Exterior RSC',
|
||||
image: '👨💻',
|
||||
testimonial: 'Migramos de sistemas obsoletos a EFC y fue la mejor decisión. Ahora somos más eficientes y competitivos.',
|
||||
rating: 5
|
||||
}
|
||||
].map((testimonial, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 p-8 border border-gray-100 hover:scale-105"
|
||||
>
|
||||
<div className="flex items-center mb-6">
|
||||
<div className="text-4xl mr-4">{testimonial.image}</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-gray-900">{testimonial.name}</h4>
|
||||
<p className="text-gray-600 text-sm">{testimonial.company}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-700 mb-4 italic">"{testimonial.testimonial}"</p>
|
||||
<div className="flex text-yellow-400">
|
||||
{[...Array(testimonial.rating)].map((_, i) => (
|
||||
<svg key={i} className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Precios */}
|
||||
<section id="precios" className="py-20 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl font-extrabold text-gray-900 mb-4">
|
||||
Planes diseñados para tu crecimiento
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
Desde agentes independientes hasta grandes corporativos
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
name: 'Starter',
|
||||
price: '$2,999',
|
||||
period: '/mes',
|
||||
description: 'Perfecto para agentes aduanales independientes',
|
||||
features: [
|
||||
'Hasta 50 pedimentos/mes',
|
||||
'5 GB almacenamiento',
|
||||
'Soporte por email',
|
||||
'Reportes básicos',
|
||||
'2 usuarios'
|
||||
],
|
||||
popular: false
|
||||
},
|
||||
{
|
||||
name: 'Professional',
|
||||
price: '$5,999',
|
||||
period: '/mes',
|
||||
description: 'Ideal para agencias medianas',
|
||||
features: [
|
||||
'Hasta 200 pedimentos/mes',
|
||||
'20 GB almacenamiento',
|
||||
'Soporte prioritario',
|
||||
'Reportes avanzados',
|
||||
'10 usuarios',
|
||||
'API acceso',
|
||||
'Integraciones SAT'
|
||||
],
|
||||
popular: true
|
||||
},
|
||||
{
|
||||
name: 'Enterprise',
|
||||
price: 'Personalizado',
|
||||
period: '',
|
||||
description: 'Para grandes corporativos',
|
||||
features: [
|
||||
'Pedimentos ilimitados',
|
||||
'Almacenamiento ilimitado',
|
||||
'Soporte 24/7 dedicado',
|
||||
'Reportes personalizados',
|
||||
'Usuarios ilimitados',
|
||||
'API completa',
|
||||
'Implementación dedicada',
|
||||
'SLA garantizado'
|
||||
],
|
||||
popular: false
|
||||
}
|
||||
].map((plan, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`relative rounded-2xl border-2 p-8 hover:scale-105 transition-all duration-300 ${
|
||||
plan.popular
|
||||
? 'border-indigo-500 bg-gradient-to-b from-indigo-50 to-white shadow-xl'
|
||||
: 'border-gray-200 bg-white hover:border-indigo-200 shadow-lg hover:shadow-xl'
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
|
||||
<span className="bg-gradient-to-r from-indigo-600 to-purple-600 text-white px-4 py-2 rounded-full text-sm font-semibold">
|
||||
Más Popular
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">{plan.name}</h3>
|
||||
<p className="text-gray-600 mb-4">{plan.description}</p>
|
||||
<div className="mb-4">
|
||||
<span className="text-4xl font-extrabold text-gray-900">{plan.price}</span>
|
||||
<span className="text-gray-600">{plan.period}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-3 mb-8">
|
||||
{plan.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-center">
|
||||
<svg className="w-5 h-5 text-green-500 mr-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span className="text-gray-700">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
onClick={() => scrollToSection('contacto')}
|
||||
className={`w-full py-3 px-6 rounded-full font-semibold transition-all duration-200 ${
|
||||
plan.popular
|
||||
? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white hover:from-indigo-700 hover:to-purple-700 shadow-lg hover:shadow-xl'
|
||||
: 'bg-gray-100 text-gray-900 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{plan.name === 'Enterprise' ? 'Contactar Ventas' : 'Comenzar Prueba'}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contacto */}
|
||||
<section id="contacto" className="py-20 bg-gradient-to-br from-indigo-900 to-purple-900">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<div className="text-white">
|
||||
<h2 className="text-4xl font-extrabold mb-6">
|
||||
¿Listo para transformar tu operación aduanal?
|
||||
</h2>
|
||||
<p className="text-xl text-indigo-200 mb-8">
|
||||
Contáctanos y descubre cómo EFC puede optimizar tus procesos de comercio exterior
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 bg-white/10 rounded-full flex items-center justify-center">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">Teléfono</h4>
|
||||
<p className="text-indigo-200">+52 (55) 1234-5678</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 bg-white/10 rounded-full flex items-center justify-center">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">Email</h4>
|
||||
<p className="text-indigo-200">contacto@aduanasoft.com</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 bg-white/10 rounded-full flex items-center justify-center">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">Oficinas</h4>
|
||||
<p className="text-indigo-200">Ciudad de México, México</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-2xl p-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-6">Solicita una demostración</h3>
|
||||
<form onSubmit={handleContactSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Nombre completo
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={contactForm.name}
|
||||
onChange={(e) => setContactForm({...contactForm, name: e.target.value})}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all duration-200"
|
||||
placeholder="Tu nombre"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Email corporativo
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={contactForm.email}
|
||||
onChange={(e) => setContactForm({...contactForm, email: e.target.value})}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all duration-200"
|
||||
placeholder="tu@empresa.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Empresa
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={contactForm.company}
|
||||
onChange={(e) => setContactForm({...contactForm, company: e.target.value})}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all duration-200"
|
||||
placeholder="Nombre de tu empresa"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Mensaje
|
||||
</label>
|
||||
<textarea
|
||||
rows="4"
|
||||
value={contactForm.message}
|
||||
onChange={(e) => setContactForm({...contactForm, message: e.target.value})}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all duration-200"
|
||||
placeholder="Cuéntanos sobre tu operación aduanal..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-gradient-to-r from-indigo-600 to-purple-600 text-white py-3 px-6 rounded-lg font-semibold hover:from-indigo-700 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
||||
>
|
||||
Enviar solicitud
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-gray-900 text-white py-12">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div className="col-span-1 md:col-span-2">
|
||||
<h3 className="text-2xl font-bold mb-4">
|
||||
<span className="bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">
|
||||
EFC
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-gray-400 mb-4 max-w-md">
|
||||
La plataforma líder para agentes aduanales e importadores, desarrollada por
|
||||
<span className="font-semibold text-indigo-400"> @AduanaSoft</span> con más de 10 años de experiencia.
|
||||
</p>
|
||||
<div className="flex space-x-4">
|
||||
<a href="#" className="text-gray-400 hover:text-white transition-colors duration-200">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" className="text-gray-400 hover:text-white transition-colors duration-200">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M22.46 6c-.77.35-1.6.58-2.46.69.88-.53 1.56-1.37 1.88-2.38-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29 0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15 0 1.49.75 2.81 1.91 3.56-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07 4.28 4.28 0 0 0 4 2.98 8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21 16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56.84-.6 1.56-1.36 2.14-2.23z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" className="text-gray-400 hover:text-white transition-colors duration-200">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">Producto</h4>
|
||||
<ul className="space-y-2 text-gray-400">
|
||||
<li><a href="#caracteristicas" className="hover:text-white transition-colors duration-200">Características</a></li>
|
||||
<li><a href="#precios" className="hover:text-white transition-colors duration-200">Precios</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors duration-200">Integraciónes</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors duration-200">API</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">Soporte</h4>
|
||||
<ul className="space-y-2 text-gray-400">
|
||||
<li><a href="#contacto" className="hover:text-white transition-colors duration-200">Contacto</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors duration-200">Documentación</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors duration-200">Centro de Ayuda</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors duration-200">Status</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
|
||||
<p>
|
||||
© 2025 EFC by <span className="font-semibold text-indigo-400">@AduanaSoft</span>.
|
||||
Todos los derechos reservados. | Solución especializada para Agentes Aduanales e Importadores.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* CSS personalizado para animaciones */}
|
||||
<style jsx>{`
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fade-in-up 0.8s ease-out;
|
||||
}
|
||||
|
||||
.hover\\:shadow-3xl:hover {
|
||||
box-shadow: 0 35px 60px -12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
313
src/pages/Login.jsx
Normal file
313
src/pages/Login.jsx
Normal file
@@ -0,0 +1,313 @@
|
||||
import React, { useState } from 'react';
|
||||
import { login } from '../api/auth';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { colors } from '../theme';
|
||||
|
||||
export default function Login() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await login(username, password);
|
||||
localStorage.setItem('access', data.access);
|
||||
localStorage.setItem('refresh', data.refresh);
|
||||
|
||||
// Obtener y guardar la información del usuario autenticado
|
||||
const apiUrl = import.meta.env.VITE_EFC_API_URL || '';
|
||||
const token = data.access;
|
||||
try {
|
||||
const res = await fetch(`${apiUrl}/user/users/me/`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (res.ok) {
|
||||
const user = await res.json();
|
||||
if (user && user.username) {
|
||||
localStorage.setItem('username', user.username);
|
||||
if (user.email) localStorage.setItem('user_email', user.email);
|
||||
if (user.id) localStorage.setItem('user_id', String(user.id));
|
||||
if (user.groups) localStorage.setItem('user_groups', JSON.stringify(user.groups));
|
||||
if (user.first_name) localStorage.setItem('user_first_name', user.first_name);
|
||||
if (user.last_name) localStorage.setItem('user_last_name', user.last_name);
|
||||
if (typeof user.is_importador !== 'undefined') localStorage.setItem('user_is_importador', String(user.is_importador));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Si falla, continuar igual
|
||||
console.error('No se pudo guardar info de usuario en localStorage', e);
|
||||
}
|
||||
|
||||
// Disparar evento personalizado para que el navbar se actualice
|
||||
window.dispatchEvent(new CustomEvent('authStateChanged'));
|
||||
|
||||
// Redirigir al dashboard
|
||||
window.location.href = '/admin';
|
||||
} catch (err) {
|
||||
setError('Usuario o contraseña incorrectos');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#F2F4F7' }}>
|
||||
{/* Background pattern */}
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
<div className="absolute inset-0" style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fillRule='evenodd'%3E%3Cg fill='%231B2A41' fillOpacity='0.1'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`
|
||||
}}></div>
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-md w-full">
|
||||
{/* Main Card */}
|
||||
<div className="bg-white backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200 overflow-hidden">
|
||||
{/* Header with navy background */}
|
||||
<div className="px-8 py-10 text-center" style={{ backgroundColor: '#1B2A41' }}>
|
||||
<div className="mb-4">
|
||||
<Link to="/" className="inline-block">
|
||||
<h1 className="text-4xl font-bold text-white">
|
||||
EFC
|
||||
</h1>
|
||||
</Link>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">
|
||||
Bienvenido de vuelta
|
||||
</h2>
|
||||
<p className="text-sm" style={{ color: 'rgba(255, 255, 255, 0.8)' }}>
|
||||
Inicia sesión para acceder a tu plataforma aduanal
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="px-8 py-8">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Username Field */}
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
|
||||
Usuario
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5" style={{ color: '#7A7A7A' }} 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>
|
||||
</div>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:border-transparent transition duration-200"
|
||||
style={{
|
||||
color: '#333333',
|
||||
borderColor: '#d1d5db',
|
||||
':focus': {
|
||||
ringColor: '#4DA6FF',
|
||||
borderColor: 'transparent'
|
||||
},
|
||||
':hover': {
|
||||
borderColor: '#4DA6FF'
|
||||
}
|
||||
}}
|
||||
placeholder="Ingresa tu usuario"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = 'transparent';
|
||||
e.target.style.boxShadow = '0 0 0 2px #4DA6FF';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (document.activeElement !== e.target) {
|
||||
e.target.style.borderColor = '#4DA6FF';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (document.activeElement !== e.target) {
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
|
||||
Contraseña
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5" style={{ color: '#7A7A7A' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
required
|
||||
className="block w-full pl-10 pr-12 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:border-transparent transition duration-200"
|
||||
style={{
|
||||
color: '#333333',
|
||||
borderColor: '#d1d5db'
|
||||
}}
|
||||
placeholder="Ingresa tu contraseña"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = 'transparent';
|
||||
e.target.style.boxShadow = '0 0 0 2px #4DA6FF';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (document.activeElement !== e.target) {
|
||||
e.target.style.borderColor = '#4DA6FF';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (document.activeElement !== e.target) {
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center transition-colors duration-200"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg className="h-5 w-5 hover:opacity-70" style={{ color: '#7A7A7A' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L8.464 8.464m1.414 1.414L12 12m0 0l2.122 2.122m0 0l1.414 1.414" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="h-5 w-5 hover:opacity-70" style={{ color: '#7A7A7A' }} 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.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="rounded-xl bg-red-50 border p-4 animate-pulse" style={{ borderColor: 'rgba(198, 40, 40, 0.2)' }}>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5" style={{ color: '#C62828' }} viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium" style={{ color: '#C62828' }}>
|
||||
{error}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Login Button */}
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
||||
style={{
|
||||
backgroundColor: '#1B2A41',
|
||||
'--tw-ring-color': '#1B2A41'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!loading) {
|
||||
e.target.style.backgroundColor = '#162234';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!loading) {
|
||||
e.target.style.backgroundColor = '#1B2A41';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" 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>
|
||||
Ingresando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Ingresar</span>
|
||||
<svg className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Additional Links */}
|
||||
<div className="text-center space-y-3">
|
||||
<div className="text-sm">
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className="font-medium transition-colors duration-200"
|
||||
style={{ color: '#4DA6FF' }}
|
||||
onMouseEnter={(e) => e.target.style.color = '#1976D2'}
|
||||
onMouseLeave={(e) => e.target.style.color = '#4DA6FF'}
|
||||
>
|
||||
¿Olvidaste tu contraseña?
|
||||
</Link>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 pt-4">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center text-sm font-medium group transition-colors duration-200"
|
||||
style={{ color: '#4DA6FF' }}
|
||||
onMouseEnter={(e) => e.target.style.color = '#1976D2'}
|
||||
onMouseLeave={(e) => e.target.style.color = '#4DA6FF'}
|
||||
>
|
||||
<svg className="mr-2 w-4 h-4 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 17l-5-5m0 0l5-5m-5 5h12" />
|
||||
</svg>
|
||||
Volver al inicio
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-8 py-4 border-t border-gray-200" style={{ backgroundColor: '#F2F4F7' }}>
|
||||
<div className="text-center">
|
||||
<p className="text-xs" style={{ color: '#7A7A7A' }}>
|
||||
Desarrollado por <span className="font-semibold" style={{ color: '#1B2A41' }}>@AduanaSoft</span>
|
||||
</p>
|
||||
<p className="text-xs mt-1" style={{ color: '#7A7A7A' }}>
|
||||
Solución especializada para Agentes Aduanales
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating elements */}
|
||||
<div className="absolute -top-4 -left-4 w-24 h-24 rounded-full blur-xl" style={{ backgroundColor: 'rgba(255, 255, 255, 0.1)' }}></div>
|
||||
<div className="absolute -bottom-4 -right-4 w-32 h-32 rounded-full blur-xl" style={{ backgroundColor: 'rgba(27, 42, 65, 0.2)' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
391
src/pages/LoginBroken.jsx
Normal file
391
src/pages/LoginBroken.jsx
Normal file
@@ -0,0 +1,391 @@
|
||||
import React, { useState } from 'react';
|
||||
import { login } from '../api/auth';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { colors } from '../theme';
|
||||
|
||||
export default function Login() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await login(username, password);
|
||||
localStorage.setItem('access', data.access);
|
||||
localStorage.setItem('refresh', data.refresh);
|
||||
|
||||
// Disparar evento personalizado para que el navbar se actualice
|
||||
window.dispatchEvent(new CustomEvent('authStateChanged'));
|
||||
|
||||
// Redirigir al dashboard
|
||||
window.location.href = '/admin';
|
||||
} catch (err) {
|
||||
setError('Usuario o contraseña incorrectos');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen bg-light-gray flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
{/* Background pattern */}
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
<div className="absolute inset-0" style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fillRule='evenodd'%3E%3Cg fill='%23${colors.primary.navy.substring(1)}' fillOpacity='0.1'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`
|
||||
}}></div>
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-md w-full">
|
||||
{/* Main Card */}
|
||||
<div className="bg-white backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200 overflow-hidden">
|
||||
{/* Header with navy background */}
|
||||
<div className="bg-navy px-8 py-10 text-center">
|
||||
<div className="mb-4">
|
||||
<Link to="/" className="inline-block">
|
||||
<h1 className="text-4xl font-bold text-white">
|
||||
EFC
|
||||
</h1>
|
||||
</Link>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">
|
||||
Bienvenido de vuelta
|
||||
</h2>
|
||||
<p className="text-white/80 text-sm">
|
||||
Inicia sesión para acceder a tu plataforma aduanal
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="px-8 py-8">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Username Field */}
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-semibold text-text-primary mb-2">
|
||||
Usuario
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-text-secondary" 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>
|
||||
</div>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl text-text-primary placeholder-text-secondary focus:outline-none focus:ring-2 focus:ring-info focus:border-transparent transition duration-200 hover:border-info/50"
|
||||
placeholder="Ingresa tu usuario"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-semibold text-text-primary mb-2">
|
||||
Contraseña
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
required
|
||||
className="block w-full pl-10 pr-12 py-3 border border-gray-300 rounded-xl text-text-primary placeholder-text-secondary focus:outline-none focus:ring-2 focus:ring-info focus:border-transparent transition duration-200 hover:border-info/50"
|
||||
placeholder="Ingresa tu contraseña"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg className="h-5 w-5 text-text-secondary hover:text-text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L8.464 8.464m1.414 1.414L12 12m0 0l2.122 2.122m0 0l1.414 1.414" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="h-5 w-5 text-text-secondary hover:text-text-primary" 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.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="rounded-xl bg-red-50 border border-error/20 p-4 animate-pulse">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-error" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-error">
|
||||
{error}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Login Button */}
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-navy hover:bg-navy-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-navy disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" 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>
|
||||
Ingresando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Ingresar</span>
|
||||
<svg className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Additional Links */}
|
||||
<div className="text-center space-y-3">
|
||||
<div className="text-sm">
|
||||
<a href="#" className="text-info hover:text-info-dark font-medium transition-colors duration-200">
|
||||
¿Olvidaste tu contraseña?
|
||||
</a>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 pt-4">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center text-info hover:text-info-dark text-sm font-medium group transition-colors duration-200"
|
||||
>
|
||||
<svg className="mr-2 w-4 h-4 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 17l-5-5m0 0l5-5m-5 5h12" />
|
||||
</svg>
|
||||
Volver al inicio
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="bg-light-gray px-8 py-4 border-t border-gray-200">
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-text-secondary">
|
||||
Desarrollado por <span className="font-semibold text-navy">@AduanaSoft</span>
|
||||
</p>
|
||||
<p className="text-xs text-text-secondary mt-1">
|
||||
Solución especializada para Agentes Aduanales
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating elements with new colors */}
|
||||
<div className="absolute -top-4 -left-4 w-24 h-24 bg-navy/10 rounded-full blur-xl"></div>
|
||||
<div className="absolute -bottom-4 -right-4 w-32 h-32 bg-info/20 rounded-full blur-xl"></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
<div className="relative max-w-md w-full">
|
||||
{/* Main Card */}
|
||||
<div className="bg-white/95 backdrop-blur-md rounded-3xl shadow-2xl border border-white/20 overflow-hidden">
|
||||
{/* Header with gradient */}
|
||||
<div className="bg-gradient-to-r from-indigo-600 to-purple-600 px-8 py-10 text-center">
|
||||
<div className="mb-4">
|
||||
<Link to="/" className="inline-block">
|
||||
<h1 className="text-4xl font-bold text-white">
|
||||
EFC
|
||||
</h1>
|
||||
</Link>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">
|
||||
Bienvenido de vuelta
|
||||
</h2>
|
||||
<p className="text-indigo-100 text-sm">
|
||||
Inicia sesión para acceder a tu plataforma aduanal
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="px-8 py-8">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Username Field */}
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Usuario
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" 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>
|
||||
</div>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition duration-200 hover:border-indigo-300"
|
||||
placeholder="Ingresa tu usuario"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Contraseña
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
required
|
||||
className="block w-full pl-10 pr-12 py-3 border border-gray-300 rounded-xl text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition duration-200 hover:border-indigo-300"
|
||||
placeholder="Ingresa tu contraseña"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg className="h-5 w-5 text-gray-400 hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L8.464 8.464m1.414 1.414L12 12m0 0l2.122 2.122m0 0l1.414 1.414" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="h-5 w-5 text-gray-400 hover:text-gray-600" 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.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="rounded-xl bg-red-50 border border-red-200 p-4 animate-pulse">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800">
|
||||
{error}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Login Button */}
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" 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>
|
||||
Ingresando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Ingresar</span>
|
||||
<svg className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Additional Links */}
|
||||
<div className="text-center space-y-3">
|
||||
<div className="text-sm">
|
||||
<a href="#" className="text-info hover:text-info-dark font-medium transition-colors duration-200">
|
||||
¿Olvidaste tu contraseña?
|
||||
</a>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 pt-4">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center text-info hover:text-info-dark text-sm font-medium group transition-colors duration-200"
|
||||
>
|
||||
<svg className="mr-2 w-4 h-4 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 17l-5-5m0 0l5-5m-5 5h12" />
|
||||
</svg>
|
||||
Volver al inicio
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="bg-light-gray px-8 py-4 border-t border-gray-200">
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-text-secondary">
|
||||
Desarrollado por <span className="font-semibold text-navy">@AduanaSoft</span>
|
||||
</p>
|
||||
<p className="text-xs text-text-secondary mt-1">
|
||||
Solución especializada para Agentes Aduanales
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating elements */}
|
||||
<div className="absolute -top-4 -left-4 w-24 h-24 bg-white/10 rounded-full blur-xl"></div>
|
||||
<div className="absolute -bottom-4 -right-4 w-32 h-32 bg-navy/20 rounded-full blur-xl"></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
286
src/pages/LoginFixed.jsx
Normal file
286
src/pages/LoginFixed.jsx
Normal file
@@ -0,0 +1,286 @@
|
||||
import React, { useState } from 'react';
|
||||
import { login } from '../api/auth';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { colors } from '../theme';
|
||||
|
||||
export default function Login() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await login(username, password);
|
||||
localStorage.setItem('access', data.access);
|
||||
localStorage.setItem('refresh', data.refresh);
|
||||
|
||||
// Disparar evento personalizado para que el navbar se actualice
|
||||
window.dispatchEvent(new CustomEvent('authStateChanged'));
|
||||
|
||||
// Redirigir al dashboard
|
||||
window.location.href = '/admin';
|
||||
} catch (err) {
|
||||
setError('Usuario o contraseña incorrectos');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#F2F4F7' }}>
|
||||
{/* Background pattern */}
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
<div className="absolute inset-0" style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fillRule='evenodd'%3E%3Cg fill='%231B2A41' fillOpacity='0.1'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`
|
||||
}}></div>
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-md w-full">
|
||||
{/* Main Card */}
|
||||
<div className="bg-white backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200 overflow-hidden">
|
||||
{/* Header with navy background */}
|
||||
<div className="px-8 py-10 text-center" style={{ backgroundColor: '#1B2A41' }}>
|
||||
<div className="mb-4">
|
||||
<Link to="/" className="inline-block">
|
||||
<h1 className="text-4xl font-bold text-white">
|
||||
EFC
|
||||
</h1>
|
||||
</Link>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">
|
||||
Bienvenido de vuelta
|
||||
</h2>
|
||||
<p className="text-sm" style={{ color: 'rgba(255, 255, 255, 0.8)' }}>
|
||||
Inicia sesión para acceder a tu plataforma aduanal
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="px-8 py-8">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Username Field */}
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
|
||||
Usuario
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5" style={{ color: '#7A7A7A' }} 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>
|
||||
</div>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:border-transparent transition duration-200"
|
||||
style={{
|
||||
color: '#333333',
|
||||
borderColor: '#d1d5db',
|
||||
':focus': {
|
||||
ringColor: '#4DA6FF',
|
||||
borderColor: 'transparent'
|
||||
},
|
||||
':hover': {
|
||||
borderColor: '#4DA6FF'
|
||||
}
|
||||
}}
|
||||
placeholder="Ingresa tu usuario"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = 'transparent';
|
||||
e.target.style.boxShadow = '0 0 0 2px #4DA6FF';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (document.activeElement !== e.target) {
|
||||
e.target.style.borderColor = '#4DA6FF';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (document.activeElement !== e.target) {
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
|
||||
Contraseña
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5" style={{ color: '#7A7A7A' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
required
|
||||
className="block w-full pl-10 pr-12 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:border-transparent transition duration-200"
|
||||
style={{
|
||||
color: '#333333',
|
||||
borderColor: '#d1d5db'
|
||||
}}
|
||||
placeholder="Ingresa tu contraseña"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = 'transparent';
|
||||
e.target.style.boxShadow = '0 0 0 2px #4DA6FF';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (document.activeElement !== e.target) {
|
||||
e.target.style.borderColor = '#4DA6FF';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (document.activeElement !== e.target) {
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center transition-colors duration-200"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg className="h-5 w-5 hover:opacity-70" style={{ color: '#7A7A7A' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L8.464 8.464m1.414 1.414L12 12m0 0l2.122 2.122m0 0l1.414 1.414" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="h-5 w-5 hover:opacity-70" style={{ color: '#7A7A7A' }} 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.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="rounded-xl bg-red-50 border p-4 animate-pulse" style={{ borderColor: 'rgba(198, 40, 40, 0.2)' }}>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5" style={{ color: '#C62828' }} viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium" style={{ color: '#C62828' }}>
|
||||
{error}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Login Button */}
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
||||
style={{
|
||||
backgroundColor: '#1B2A41',
|
||||
'--tw-ring-color': '#1B2A41'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!loading) {
|
||||
e.target.style.backgroundColor = '#162234';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!loading) {
|
||||
e.target.style.backgroundColor = '#1B2A41';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" 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>
|
||||
Ingresando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Ingresar</span>
|
||||
<svg className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Additional Links */}
|
||||
<div className="text-center space-y-3">
|
||||
<div className="text-sm">
|
||||
<a href="#" className="font-medium transition-colors duration-200" style={{ color: '#4DA6FF' }}
|
||||
onMouseEnter={(e) => e.target.style.color = '#1976D2'}
|
||||
onMouseLeave={(e) => e.target.style.color = '#4DA6FF'}
|
||||
>
|
||||
¿Olvidaste tu contraseña?
|
||||
</a>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 pt-4">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center text-sm font-medium group transition-colors duration-200"
|
||||
style={{ color: '#4DA6FF' }}
|
||||
onMouseEnter={(e) => e.target.style.color = '#1976D2'}
|
||||
onMouseLeave={(e) => e.target.style.color = '#4DA6FF'}
|
||||
>
|
||||
<svg className="mr-2 w-4 h-4 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 17l-5-5m0 0l5-5m-5 5h12" />
|
||||
</svg>
|
||||
Volver al inicio
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-8 py-4 border-t border-gray-200" style={{ backgroundColor: '#F2F4F7' }}>
|
||||
<div className="text-center">
|
||||
<p className="text-xs" style={{ color: '#7A7A7A' }}>
|
||||
Desarrollado por <span className="font-semibold" style={{ color: '#1B2A41' }}>@AduanaSoft</span>
|
||||
</p>
|
||||
<p className="text-xs mt-1" style={{ color: '#7A7A7A' }}>
|
||||
Solución especializada para Agentes Aduanales
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating elements */}
|
||||
<div className="absolute -top-4 -left-4 w-24 h-24 rounded-full blur-xl" style={{ backgroundColor: 'rgba(255, 255, 255, 0.1)' }}></div>
|
||||
<div className="absolute -bottom-4 -right-4 w-32 h-32 rounded-full blur-xl" style={{ backgroundColor: 'rgba(27, 42, 65, 0.2)' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
158
src/pages/Notificaciones.jsx
Normal file
158
src/pages/Notificaciones.jsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { fetchNotificaciones, fetchAllNotifications, marcarNotificacionComoVista } from '../api/notificaciones';
|
||||
|
||||
export default function Notificaciones() {
|
||||
const [notificaciones, setNotificaciones] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const pageSize = 15;
|
||||
const [count, setCount] = useState(0);
|
||||
const [filtroVisto, setFiltroVisto] = useState('todos');
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
let data;
|
||||
if (filtroVisto === 'todos') {
|
||||
data = await fetchAllNotifications({ page, pageSize });
|
||||
} else {
|
||||
const params = { page, pageSize };
|
||||
if (filtroVisto === 'visto') params.visto = true;
|
||||
else if (filtroVisto === 'novisto') params.visto = false;
|
||||
data = await fetchNotificaciones(params);
|
||||
}
|
||||
setNotificaciones(data.results);
|
||||
setCount(data.count);
|
||||
} catch (e) {
|
||||
setError('Error al cargar notificaciones');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
// eslint-disable-next-line
|
||||
}, [page, filtroVisto]);
|
||||
|
||||
const handleActualizarTodas = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await Promise.all(
|
||||
notificaciones.filter(n => !n.visto).map(n => marcarNotificacionComoVista(n.id))
|
||||
);
|
||||
fetchData();
|
||||
} catch (e) {
|
||||
setError('Error al actualizar notificaciones');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFiltroChange = (e) => {
|
||||
setFiltroVisto(e.target.value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-5xl mx-auto">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-4 gap-2">
|
||||
<h1 className="text-2xl font-bold">Notificaciones</h1>
|
||||
<div className="flex gap-2 items-center">
|
||||
<select
|
||||
className="border rounded px-2 py-1"
|
||||
value={filtroVisto}
|
||||
onChange={handleFiltroChange}
|
||||
>
|
||||
<option value="todos">Todas</option>
|
||||
<option value="visto">Vistas</option>
|
||||
<option value="novisto">No vistas</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={handleActualizarTodas}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded shadow"
|
||||
disabled={loading}
|
||||
>
|
||||
Actualizar todas como leídas
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="text-blue-500 py-8 text-center">Cargando...</div>
|
||||
) : error ? (
|
||||
<div className="text-red-500 py-8 text-center">{error}</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden">
|
||||
<thead className="bg-gradient-to-r from-gray-50 sticky top-0 z-20">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200">ID</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Tipo</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Mensaje</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider border-b border-gray-200">Fecha</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200">Visto</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-100" style={{ position: 'relative', minHeight: 'calc(8 * 56px)' }}>
|
||||
{Array.from({ length: pageSize }).map((_, idx) => {
|
||||
const n = notificaciones[idx];
|
||||
if (n) {
|
||||
return (
|
||||
<tr key={n.id} className={`transition-all duration-200 hover:bg-blue-100 hover:shadow-lg ${n.visto ? '' : 'bg-blue-50'}`}>
|
||||
<td className="px-6 py-4 text-center align-middle">{n.id}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle font-medium text-blue-900">{n.tipo?.descripcion || n.tipo?.tipo}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-800">{n.mensaje}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap align-middle text-gray-700">{new Date(n.fecha_envio || n.created_at).toLocaleString()}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-center align-middle">
|
||||
{n.visto ? (
|
||||
<span className="text-green-600 font-semibold">Sí</span>
|
||||
) : (
|
||||
<span className="text-red-600 font-semibold">No</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<tr key={"empty-" + idx}>
|
||||
<td className="px-6 py-4 text-center text-gray-300">-</td>
|
||||
<td className="px-6 py-4 text-gray-300">-</td>
|
||||
<td className="px-6 py-4 text-gray-300">-</td>
|
||||
<td className="px-6 py-4 text-gray-300">-</td>
|
||||
<td className="px-6 py-4 text-center text-gray-300">-</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{/* Paginación */}
|
||||
<div className="flex justify-between items-center mt-4">
|
||||
<div>
|
||||
Página {page} de {Math.ceil(count / pageSize) || 1}
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
className="px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 disabled:opacity-50"
|
||||
>
|
||||
Anterior
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage((p) => (p * pageSize < count ? p + 1 : p))}
|
||||
disabled={page * pageSize >= count}
|
||||
className="px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 disabled:opacity-50"
|
||||
>
|
||||
Siguiente
|
||||
</button>
|
||||
<span className="ml-2 text-sm text-gray-500">15 por página</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
300
src/pages/Organization.jsx
Normal file
300
src/pages/Organization.jsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { fetchOrganizationUsage } from '../api/organization';
|
||||
import { useNotification } from '../context/NotificationContext';
|
||||
import '../assets/organization-animations.css';
|
||||
|
||||
export default function Organization() {
|
||||
const [info, setInfo] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const { showMessage } = useNotification();
|
||||
// Estado para animar el progress bar
|
||||
const [animatedPercent, setAnimatedPercent] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('access');
|
||||
if (!token) {
|
||||
setError('No se encontró el token de acceso.');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
fetchOrganizationUsage(token)
|
||||
.then(data => {
|
||||
setInfo(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.message === 'SESSION_EXPIRED') {
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
} else {
|
||||
setError(err.message);
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
}, [showMessage]);
|
||||
|
||||
// Animación del progress bar
|
||||
useEffect(() => {
|
||||
if (!info) return;
|
||||
const used = info.espacio_utilizado_gb || 0;
|
||||
const limit = info.limite_almacenamiento_gb || 1;
|
||||
const percent = Math.min(100, (100 * used / limit));
|
||||
let start = 0;
|
||||
// Si ya está en el valor correcto, no animar
|
||||
if (animatedPercent === percent) return;
|
||||
// Animar de 0 a percent
|
||||
const step = () => {
|
||||
setAnimatedPercent(prev => {
|
||||
if (prev < percent) {
|
||||
const next = Math.min(prev + 2, percent); // velocidad de animación
|
||||
if (next < percent) {
|
||||
setTimeout(step, 10);
|
||||
}
|
||||
return next;
|
||||
} else {
|
||||
return percent;
|
||||
}
|
||||
});
|
||||
};
|
||||
setAnimatedPercent(0);
|
||||
setTimeout(step, 200); // pequeño delay para que se note la animación
|
||||
// eslint-disable-next-line
|
||||
}, [info]);
|
||||
|
||||
if (loading) return (
|
||||
<div className="h-full bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<svg className="animate-spin h-12 w-12 text-navy-600 mx-auto mb-4" 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>
|
||||
<p className="text-gray-600 text-lg">Cargando información de la organización...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (error) return (
|
||||
<div className="h-full bg-gray-50 flex items-center justify-center">
|
||||
<div className="bg-danger-50 border border-danger-200 rounded-xl p-6 max-w-md shadow-lg">
|
||||
<div className="flex items-center">
|
||||
<svg className="h-6 w-6 text-danger-500 mr-3" 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"></path>
|
||||
</svg>
|
||||
<p className="text-danger-800 font-medium">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 p-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Header mejorado y 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 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
|
||||
<div className="flex-shrink-0 bg-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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M3 7c0 2.21 3.582 4 8 4s8-1.79 8-4M3 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
||||
Mi Organización
|
||||
{info && (
|
||||
<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">
|
||||
{info.total_usuarios} usuarios
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
<p className="text-lg text-blue-700/80 font-medium">Información y métricas de uso de tu organización</p>
|
||||
</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>
|
||||
|
||||
{/* Barra de almacenamiento con color y progress bar */}
|
||||
<div className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<svg className="w-6 h-6 text-success-600 mr-2" 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 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||||
</svg>
|
||||
Uso de Almacenamiento
|
||||
</h2>
|
||||
<div className="relative w-full h-8 bg-gray-200 rounded-full overflow-hidden shadow-inner">
|
||||
{/* Progress bar de color dinámico según porcentaje */}
|
||||
{(() => {
|
||||
const used = info?.espacio_utilizado_gb || 0;
|
||||
const limit = info?.limite_almacenamiento_gb || 1;
|
||||
const percent = Math.min(100, (100 * used / limit));
|
||||
let barColor = 'linear-gradient(90deg, #22c55e 0%, #16a34a 100%)'; // verde
|
||||
if (animatedPercent >= 80) {
|
||||
barColor = 'linear-gradient(90deg, #ef4444 0%, #b91c1c 100%)'; // rojo
|
||||
} else if (animatedPercent >= 50) {
|
||||
barColor = 'linear-gradient(90deg, #f59e42 0%, #d97706 100%)'; // naranja
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="absolute left-0 top-0 h-8 rounded-full shadow-lg transition-all duration-700"
|
||||
style={{ width: `${animatedPercent}%`, background: barColor }}
|
||||
></div>
|
||||
);
|
||||
})()}
|
||||
{/* Etiquetas sobre la barra */}
|
||||
<div className="absolute left-0 top-0 w-full h-8 flex items-center justify-between px-4 text-sm font-semibold z-10">
|
||||
<span className="text-success-700 flex items-center">
|
||||
<span className="inline-block w-3 h-3 rounded-full bg-gradient-to-br from-green-400 to-green-600 mr-2"></span>
|
||||
{info?.espacio_utilizado_gb?.toFixed(2)} GB usados
|
||||
</span>
|
||||
<span className="text-gray-700">{info?.espacio_disponible_bytes ? (info.espacio_disponible_bytes / (1024 * 1024 * 1024)).toFixed(2) : 0} GB libres</span>
|
||||
<span className="text-gray-500">{info?.limite_almacenamiento_gb} GB límite</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{/* Tarjeta Organización */}
|
||||
<div className="bg-white rounded-lg shadow-md border border-navy-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '0ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-blue-700 rounded-lg flex items-center justify-center shadow-md mb-2">
|
||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M3 7c0 2.21 3.582 4 8 4s8-1.79 8-4M3 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-navy-700 font-semibold mb-1">Organización</span>
|
||||
<span className="text-2xl font-bold text-navy-900">{info?.organizacion}</span>
|
||||
</div>
|
||||
{/* Tarjeta Usuarios */}
|
||||
<div className="bg-white rounded-lg shadow-md border border-primary-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '50ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-green-700 rounded-lg flex items-center justify-center shadow-md mb-2">
|
||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a4 4 0 00-3-3.87M9 20H4v-2a4 4 0 013-3.87m9-4a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-primary-700 font-semibold mb-1">Usuarios</span>
|
||||
<span className="text-2xl font-bold text-primary-900">{info?.total_usuarios}</span>
|
||||
</div>
|
||||
{/* Tarjeta Pedimentos */}
|
||||
<div className="bg-white rounded-lg shadow-md border border-success-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '100ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-orange-400 to-orange-600 rounded-lg flex items-center justify-center shadow-md mb-2">
|
||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17v-6a2 2 0 012-2h2a2 2 0 012 2v6m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-success-700 font-semibold mb-1">Pedimentos</span>
|
||||
<span className="text-2xl font-bold text-success-900">{info?.total_pedimentos}</span>
|
||||
</div>
|
||||
{/* Tarjeta Documentos */}
|
||||
<div className="bg-white rounded-lg shadow-md border border-warning-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '150ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-yellow-400 to-yellow-600 rounded-lg flex items-center justify-center shadow-md mb-2">
|
||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 7h10M7 11h10M7 15h6M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-warning-700 font-semibold mb-1">Documentos</span>
|
||||
<span className="text-2xl font-bold text-warning-900">{info?.total_documentos}</span>
|
||||
</div>
|
||||
{/* Tarjeta Límite de Almacenamiento */}
|
||||
<div className="bg-white rounded-lg shadow-md border border-light-gray-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '200ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-gray-400 to-gray-600 rounded-lg flex items-center justify-center shadow-md mb-2">
|
||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-light-gray-700 font-semibold mb-1">Límite de Almacenamiento</span>
|
||||
<span className="text-2xl font-bold text-light-gray-900">{info?.limite_almacenamiento_gb} GB</span>
|
||||
</div>
|
||||
{/* Tarjeta Espacio Utilizado */}
|
||||
<div className="bg-white rounded-lg shadow-md border border-warning-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '250ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-pink-400 to-pink-600 rounded-lg flex items-center justify-center shadow-md mb-2">
|
||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v8m0 0a4 4 0 100-8 4 4 0 000 8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-warning-700 font-semibold mb-1">Espacio Utilizado</span>
|
||||
<span className="text-2xl font-bold text-warning-900">{info?.espacio_utilizado_gb?.toFixed(2)} GB</span>
|
||||
</div>
|
||||
{/* Tarjeta Espacio Disponible */}
|
||||
<div className="bg-white rounded-lg shadow-md border border-success-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '300ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-green-400 to-green-600 rounded-lg flex items-center justify-center shadow-md mb-2">
|
||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 20V4m0 0a8 8 0 110 16 8 8 0 010-16z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-success-700 font-semibold mb-1">Espacio Disponible</span>
|
||||
<span className="text-2xl font-bold text-success-900">{info?.espacio_disponible_bytes ? (info.espacio_disponible_bytes / (1024 * 1024 * 1024)).toFixed(2) : 0} GB</span>
|
||||
</div>
|
||||
{/* Tarjeta Porcentaje Utilizado */}
|
||||
<div className="bg-white rounded-lg shadow-md border border-accent-200 p-4 flex flex-col items-center min-w-0 opacity-0 translate-y-6 animate-fadein-slideup transition-all duration-500 hover:scale-110 hover:shadow-2xl" style={{ animationDelay: '350ms', animationFillMode: 'forwards', transitionTimingFunction: 'cubic-bezier(0.22,1,0.36,1)' }}>
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-purple-400 to-purple-700 rounded-lg flex items-center justify-center shadow-md mb-2">
|
||||
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" strokeWidth="2" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6l4 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-accent-700 font-semibold mb-1">Porcentaje Utilizado</span>
|
||||
<span className="text-2xl font-bold text-accent-900">{info?.porcentaje_utilizado}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Acciones */}
|
||||
<div className="mt-8 bg-white rounded-xl shadow-lg border border-gray-200 p-6">
|
||||
<div className="flex items-center mb-6">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-primary-500 to-primary-600 rounded-lg flex items-center justify-center shadow-lg mr-4">
|
||||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Acciones</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-sm font-medium rounded-lg text-white bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200 transform hover:scale-105 shadow-lg">
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Editar Organización
|
||||
</button>
|
||||
|
||||
<button className="inline-flex items-center justify-center px-6 py-3 border border-gray-300 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-primary-500 transition-all duration-200 transform hover:scale-105 shadow-lg">
|
||||
<svg className="w-5 h-5 mr-2" 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>
|
||||
Ver Reportes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
212
src/pages/PasswordResetConfirm.jsx
Normal file
212
src/pages/PasswordResetConfirm.jsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import React, { useState } from 'react';
|
||||
import logo from '../assets/react.svg';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function PasswordResetConfirm() {
|
||||
const { uid, token } = useParams();
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
if (newPassword !== confirmPassword) {
|
||||
setError('Las contraseñas no coinciden.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
// POST a la ruta real del backend (URL absoluta)
|
||||
const apiUrl = `http://192.168.1.195:8000/api/v1/user/password-reset-confirm/${uid}/${token}/`;
|
||||
const res = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: newPassword }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
setError(data.detail || 'No se pudo cambiar la contraseña.');
|
||||
} else {
|
||||
setSuccess(true);
|
||||
setTimeout(() => navigate('/login'), 2500);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Error de red. Intenta de nuevo.');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8" style={{ backgroundColor: '#F2F4F7' }}>
|
||||
{/* Background pattern */}
|
||||
<div className="absolute inset-0 opacity-20 pointer-events-none select-none">
|
||||
<div className="absolute inset-0" style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fillRule='evenodd'%3E%3Cg fill='%231B2A41' fillOpacity='0.1'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`
|
||||
}}></div>
|
||||
</div>
|
||||
<div className="relative max-w-md w-full">
|
||||
<div className="bg-white backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200 overflow-hidden">
|
||||
{/* Header navy */}
|
||||
<div className="px-8 py-10 text-center" style={{ backgroundColor: '#1B2A41' }}>
|
||||
{/* Logo eliminado para diseño limpio */}
|
||||
<h2 className="text-2xl font-bold text-white mb-2">Nueva contraseña</h2>
|
||||
<p className="text-sm" style={{ color: 'rgba(255, 255, 255, 0.8)' }}>
|
||||
Ingresa tu nueva contraseña y confírmala
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-8 py-8">
|
||||
{success ? (
|
||||
<div className="text-green-700 bg-green-50 border border-green-200 rounded p-4 text-center mb-4">
|
||||
Contraseña cambiada correctamente. Redirigiendo al login...
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Nueva contraseña */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
|
||||
Nueva contraseña
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5" style={{ color: '#7A7A7A' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:border-transparent transition duration-200"
|
||||
style={{ color: '#333333', borderColor: '#d1d5db' }}
|
||||
placeholder="Nueva contraseña"
|
||||
value={newPassword}
|
||||
onChange={e => setNewPassword(e.target.value)}
|
||||
onFocus={e => { e.target.style.borderColor = 'transparent'; e.target.style.boxShadow = '0 0 0 2px #4DA6FF'; }}
|
||||
onBlur={e => { e.target.style.borderColor = '#d1d5db'; e.target.style.boxShadow = 'none'; }}
|
||||
onMouseEnter={e => { if (document.activeElement !== e.target) e.target.style.borderColor = '#4DA6FF'; }}
|
||||
onMouseLeave={e => { if (document.activeElement !== e.target) e.target.style.borderColor = '#d1d5db'; }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Confirmar contraseña */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#333333' }}>
|
||||
Confirmar contraseña
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5" style={{ color: '#7A7A7A' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:border-transparent transition duration-200"
|
||||
style={{ color: '#333333', borderColor: '#d1d5db' }}
|
||||
placeholder="Confirmar contraseña"
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
onFocus={e => { e.target.style.borderColor = 'transparent'; e.target.style.boxShadow = '0 0 0 2px #4DA6FF'; }}
|
||||
onBlur={e => { e.target.style.borderColor = '#d1d5db'; e.target.style.boxShadow = 'none'; }}
|
||||
onMouseEnter={e => { if (document.activeElement !== e.target) e.target.style.borderColor = '#4DA6FF'; }}
|
||||
onMouseLeave={e => { if (document.activeElement !== e.target) e.target.style.borderColor = '#d1d5db'; }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="rounded-xl bg-red-50 border p-4 animate-pulse" style={{ borderColor: 'rgba(198, 40, 40, 0.2)' }}>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5" style={{ color: '#C62828' }} viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium" style={{ color: '#C62828' }}>{error}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Botón */}
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-semibold rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
|
||||
style={{ backgroundColor: '#1B2A41', '--tw-ring-color': '#1B2A41' }}
|
||||
onMouseEnter={e => { if (!loading) e.target.style.backgroundColor = '#162234'; }}
|
||||
onMouseLeave={e => { if (!loading) e.target.style.backgroundColor = '#1B2A41'; }}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" 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>
|
||||
Cambiando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Cambiar contraseña</span>
|
||||
<svg className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{/* Link volver al login */}
|
||||
<div className="text-center space-y-3 mt-4">
|
||||
<div className="text-sm">
|
||||
<a
|
||||
href="/login"
|
||||
className="font-medium transition-colors duration-200"
|
||||
style={{ color: '#4DA6FF' }}
|
||||
onMouseEnter={e => e.target.style.color = '#1976D2'}
|
||||
onMouseLeave={e => e.target.style.color = '#4DA6FF'}
|
||||
>
|
||||
Volver al login
|
||||
</a>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 pt-4">
|
||||
<a
|
||||
href="/"
|
||||
className="inline-flex items-center text-sm font-medium group transition-colors duration-200"
|
||||
style={{ color: '#4DA6FF' }}
|
||||
onMouseEnter={e => e.target.style.color = '#1976D2'}
|
||||
onMouseLeave={e => e.target.style.color = '#4DA6FF'}
|
||||
>
|
||||
<svg className="mr-2 w-4 h-4 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 17l-5-5m0 0l5-5m-5 5h12" />
|
||||
</svg>
|
||||
Volver al inicio
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
{/* Footer */}
|
||||
<div className="px-8 py-4 border-t border-gray-200" style={{ backgroundColor: '#F2F4F7' }}>
|
||||
<div className="text-center">
|
||||
<p className="text-xs" style={{ color: '#7A7A7A' }}>
|
||||
Desarrollado por <span className="font-semibold" style={{ color: '#1B2A41' }}>@AduanaSoft</span>
|
||||
</p>
|
||||
<p className="text-xs mt-1" style={{ color: '#7A7A7A' }}>
|
||||
Solución especializada para Agentes Aduanales
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Floating elements */}
|
||||
<div className="absolute -top-4 -left-4 w-24 h-24 rounded-full blur-xl" style={{ backgroundColor: 'rgba(255, 255, 255, 0.1)' }}></div>
|
||||
<div className="absolute -bottom-4 -right-4 w-32 h-32 rounded-full blur-xl" style={{ backgroundColor: 'rgba(27, 42, 65, 0.2)' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
930
src/pages/PedimentoDetail.jsx
Normal file
930
src/pages/PedimentoDetail.jsx
Normal file
@@ -0,0 +1,930 @@
|
||||
import React, { useEffect, 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-pedimento')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'fadein-slideup-pedimento';
|
||||
style.innerHTML = fadeInSlideUp;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import xml from 'highlight.js/lib/languages/xml';
|
||||
import 'highlight.js/styles/github.css';
|
||||
hljs.registerLanguage('xml', xml);
|
||||
// import type removed for JSX compatibility
|
||||
import { fetchPedimentoDocuments } from '../api/pedimentoDocuments';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { useNotification } from '../context/NotificationContext';
|
||||
|
||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||
|
||||
const downloadFile = async (id, filename = 'archivo', showMessage) => {
|
||||
const token = localStorage.getItem('access');
|
||||
const res = await fetch(`${API_URL}/record/documents/descargar/${id}/`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (res.status === 401) {
|
||||
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
alert('No autorizado o error en la descarga');
|
||||
return;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const downloadBulkZip = async (ids, showMessage, pedimentoNombre) => {
|
||||
if (!ids.length) {
|
||||
showMessage('Selecciona al menos un documento.', 'error');
|
||||
return;
|
||||
}
|
||||
const token = localStorage.getItem('access');
|
||||
const res = await fetch(`${API_URL}/record/documents/bulk-download/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ document_ids: ids, pedimento_nombre: pedimentoNombre }),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
showMessage('No autorizado o error en la descarga masiva', 'error');
|
||||
return;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${pedimentoNombre || 'documentos'}.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
import { useRef, useLayoutEffect } from 'react';
|
||||
export default function PedimentoDetail() {
|
||||
// Función para formatear XML (pretty print)
|
||||
function formatXml(xml) {
|
||||
const PADDING = ' ';
|
||||
const reg = /(>)(<)(\/*)/g;
|
||||
let formatted = '';
|
||||
let pad = 0;
|
||||
xml = xml.replace(reg, '$1\r\n$2$3');
|
||||
xml.split(/\r?\n/).forEach((node) => {
|
||||
let indent = 0;
|
||||
if (node.match(/.+<\/\w[^>]*>$/)) {
|
||||
indent = 0;
|
||||
} else if (node.match(/^<\/\w/)) {
|
||||
if (pad !== 0) pad -= 1;
|
||||
} else if (node.match(/^<\w[^>]*[^\/]>/)) {
|
||||
indent = 1;
|
||||
}
|
||||
formatted += PADDING.repeat(pad) + node + '\r\n';
|
||||
pad += indent;
|
||||
});
|
||||
return formatted.trim();
|
||||
}
|
||||
// Helper para obtener el nombre legible del tipo de documento
|
||||
const getDocumentTypeName = (type) => {
|
||||
const found = documentTypeOptions.find(opt => String(opt.value) === String(type));
|
||||
return found ? found.label : 'Documento';
|
||||
};
|
||||
// Estado para modal de preview
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewUrl, setPreviewUrl] = useState('');
|
||||
const [previewType, setPreviewType] = useState('');
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
const [previewError, setPreviewError] = useState('');
|
||||
const [previewXml, setPreviewXml] = useState('');
|
||||
const [previewXmlHtml, setPreviewXmlHtml] = useState('');
|
||||
// Filtros y ordenamiento
|
||||
const [fileNameFilter, setFileNameFilter] = useState('');
|
||||
const [extensionFilter, setExtensionFilter] = useState('');
|
||||
const [dateFilter, setDateFilter] = useState('');
|
||||
const [orderBy, setOrderBy] = useState('');
|
||||
const [orderDir, setOrderDir] = useState('asc');
|
||||
const { id } = useParams();
|
||||
const [pedimento, setPedimento] = useState(null);
|
||||
const [docsLoading, setDocsLoading] = useState(true);
|
||||
const [docsError, setDocsError] = useState('');
|
||||
const [documents, setDocuments] = useState([]);
|
||||
const [docsCount, setDocsCount] = useState(0);
|
||||
const [docsNext, setDocsNext] = useState(null);
|
||||
const [docsPrev, setDocsPrev] = useState(null);
|
||||
// Refuerza la paginación SPA: nunca recarga la página, solo cambia el estado local
|
||||
const [page, setPage] = useState(1);
|
||||
// Ref para foco oculto (accesibilidad, opcional)
|
||||
const focusKeeperRef = useRef(null);
|
||||
|
||||
// Handler SPA para paginación
|
||||
const handlePageChange = (newPage, e) => {
|
||||
if (e && typeof e.preventDefault === 'function') e.preventDefault();
|
||||
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
|
||||
if (newPage < 1 || newPage > Math.max(1, Math.ceil(docsCount / pageSize)) || newPage === page) return;
|
||||
setPage(newPage);
|
||||
// Quitar el foco del botón activo para evitar salto de scroll
|
||||
if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
|
||||
// Eliminado manejo manual de scroll para evitar saltos
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [documentTypeFilter, setDocumentTypeFilter] = useState('');
|
||||
const documentTypeOptions = [
|
||||
{ value: '', label: 'Todos' },
|
||||
{ value: 1, label: 'Pedimento Partida' },
|
||||
{ value: 2, label: 'Pedimento Completo' },
|
||||
{ value: 3, label: 'Pedimento Remesas' },
|
||||
{ value: 4, label: 'Pedimento Acuse' },
|
||||
{ value: 5, label: 'Pedimento EDocument' },
|
||||
{ value: 6, label: 'Estado Pedimento' },
|
||||
];
|
||||
const { showMessage } = useNotification();
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('access');
|
||||
fetch(`${API_URL}/customs/pedimentos/${id}/`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status === 401) {
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
return null;
|
||||
}
|
||||
if (!res.ok) throw new Error('No autorizado o error en la petición');
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
setPedimento(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
setError(err.message);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [id, showMessage]);
|
||||
|
||||
// Fetch paginated documents
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
const token = localStorage.getItem('access');
|
||||
setDocsLoading(true);
|
||||
setDocsError('');
|
||||
fetchPedimentoDocuments(token, id, page, pageSize)
|
||||
.then((data) => {
|
||||
setDocuments(data.results);
|
||||
setDocsCount(data.count);
|
||||
setDocsNext(data.next);
|
||||
setDocsPrev(data.previous);
|
||||
setDocsLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.message === 'SESSION_EXPIRED') {
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
} else {
|
||||
setDocsError(err.message);
|
||||
}
|
||||
setDocsLoading(false);
|
||||
});
|
||||
}, [id, page, pageSize, showMessage]);
|
||||
|
||||
if (loading) return (
|
||||
<div className="h-full bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<svg className="animate-spin h-12 w-12 text-navy-600 mx-auto mb-4" 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>
|
||||
<p className="text-gray-600 text-lg">Cargando detalle de pedimento...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (error) return (
|
||||
<div className="h-full bg-gray-50 flex items-center justify-center">
|
||||
<div className="bg-danger-50 border border-danger-200 rounded-xl p-6 max-w-md shadow-lg">
|
||||
<div className="flex items-center">
|
||||
<svg className="h-6 w-6 text-danger-500 mr-3" 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"></path>
|
||||
</svg>
|
||||
<p className="text-danger-800 font-medium">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (!pedimento) return null;
|
||||
|
||||
const allDocIds = documents.map(doc => doc.id);
|
||||
const allSelected = selected.length === allDocIds.length && allDocIds.length > 0;
|
||||
|
||||
const handleSelect = (id) => {
|
||||
setSelected(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (allSelected) setSelected([]);
|
||||
else setSelected(allDocIds);
|
||||
};
|
||||
|
||||
const handleBulkDownload = async (ids) => {
|
||||
setDownloading(true);
|
||||
await downloadBulkZip(ids, showMessage, pedimento?.pedimento);
|
||||
setDownloading(false);
|
||||
};
|
||||
|
||||
// Vista previa de documento
|
||||
const handlePreview = async (doc) => {
|
||||
setPreviewLoading(true);
|
||||
setPreviewError('');
|
||||
setPreviewUrl('');
|
||||
setPreviewType('');
|
||||
setPreviewXml('');
|
||||
setPreviewOpen(true);
|
||||
try {
|
||||
const token = localStorage.getItem('access');
|
||||
const res = await fetch(`${API_URL}/record/documents/descargar/${doc.id}/`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (res.status === 401) {
|
||||
setPreviewError('Tu sesión ha expirado, por favor inicia sesión de nuevo.');
|
||||
setPreviewLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
setPreviewError('No autorizado o error en la descarga');
|
||||
setPreviewLoading(false);
|
||||
return;
|
||||
}
|
||||
// Detectar tipo de archivo
|
||||
let type = '';
|
||||
if (doc.extension) {
|
||||
if (doc.extension.toLowerCase() === 'pdf') type = 'pdf';
|
||||
else if (["jpg","jpeg","png","gif","bmp","webp"].includes(doc.extension.toLowerCase())) type = 'img';
|
||||
else if (doc.extension.toLowerCase() === 'xml') type = 'xml';
|
||||
else type = 'other';
|
||||
}
|
||||
setPreviewType(type);
|
||||
if (type === 'xml') {
|
||||
const text = await res.text();
|
||||
const prettyText = formatXml(text);
|
||||
setPreviewXml(prettyText);
|
||||
// Formatear y resaltar XML
|
||||
try {
|
||||
const highlighted = hljs.highlight(prettyText, { language: 'xml' }).value;
|
||||
setPreviewXmlHtml(highlighted);
|
||||
} catch (e) {
|
||||
setPreviewXmlHtml(prettyText);
|
||||
}
|
||||
setPreviewLoading(false);
|
||||
} else {
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
setPreviewUrl(url);
|
||||
setPreviewLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setPreviewError('Error al obtener el archivo');
|
||||
setPreviewLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Cerrar modal y limpiar blob
|
||||
const handleClosePreview = () => {
|
||||
setPreviewOpen(false);
|
||||
if (previewUrl) window.URL.revokeObjectURL(previewUrl);
|
||||
setPreviewUrl('');
|
||||
setPreviewType('');
|
||||
setPreviewError('');
|
||||
setPreviewXml('');
|
||||
setPreviewXmlHtml('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-50 h-full flex flex-col">
|
||||
{/* Modal de vista previa resizable */}
|
||||
{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">
|
||||
<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>
|
||||
</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"
|
||||
>
|
||||
<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">
|
||||
{previewLoading ? (
|
||||
<div className="text-center py-8 text-gray-500 flex-1 flex items-center justify-center">Cargando documento...</div>
|
||||
) : previewError ? (
|
||||
<div className="text-center py-8 text-danger-600 flex-1 flex items-center justify-center">{previewError}</div>
|
||||
) : previewType === 'pdf' ? (
|
||||
<iframe src={previewUrl} title="PDF Preview" className="border rounded flex-1" style={{ width: '100%', height: '100%' }} />
|
||||
) : 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' }} />
|
||||
) : 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>
|
||||
<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>
|
||||
</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%'
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: previewXmlHtml }}
|
||||
/>
|
||||
</div>
|
||||
) : previewUrl ? (
|
||||
<a href={previewUrl} target="_blank" rel="noopener noreferrer" className="text-blue-600 underline">Descargar archivo</a>
|
||||
) : 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">
|
||||
<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">
|
||||
<Link
|
||||
to="/expedientes"
|
||||
className="inline-flex items-center text-blue-600 hover:text-blue-800 transition-colors duration-200 mb-4"
|
||||
>
|
||||
<svg className="w-5 h-5 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>
|
||||
</Link>
|
||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
||||
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>
|
||||
</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"
|
||||
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>
|
||||
<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>
|
||||
</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
|
||||
</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">
|
||||
{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>
|
||||
{pedimento.existe_expediente ? 'Disponible' : 'No disponible'}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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"
|
||||
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">
|
||||
<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">
|
||||
📄 {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>
|
||||
{allDocIds.length > 0 && (
|
||||
<div className="flex space-x-3">
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
{downloading ? 'Descargando...' : 'Descargar todos'}
|
||||
</button>
|
||||
|
||||
<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"
|
||||
>
|
||||
<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" />
|
||||
</svg>
|
||||
Descargar seleccionados ({selected.length})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</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">
|
||||
<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' }}>
|
||||
<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' }}
|
||||
/>
|
||||
</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={() => {
|
||||
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={() => {
|
||||
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={() => {
|
||||
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={() => {
|
||||
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={() => {
|
||||
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>
|
||||
</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 ? (
|
||||
<>
|
||||
{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;
|
||||
}
|
||||
if (aVal < bVal) return orderDir === 'asc' ? -1 : 1;
|
||||
if (aVal > bVal) return orderDir === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
})
|
||||
.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' }}>
|
||||
<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' }}
|
||||
/>
|
||||
</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'}>
|
||||
{doc.archivo ? doc.archivo.split('/').pop() : 'Sin nombre'}
|
||||
</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-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">
|
||||
{doc.extension || 'N/A'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-2 py-2 whitespace-nowrap text-[13px] text-gray-700 align-middle">
|
||||
{doc.size || 'N/A'}
|
||||
</td>
|
||||
<td className="px-2 py-2 whitespace-nowrap text-[13px] text-gray-700 align-middle">
|
||||
{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">
|
||||
<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"
|
||||
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="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"
|
||||
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>
|
||||
))}
|
||||
{/* 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"
|
||||
>
|
||||
{[5, 10, 20, 50, 100, 200, 400,600, 1200, 2400, 10000].map(size => (
|
||||
<option key={size} value={size}>{size}</option>
|
||||
))}
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
385
src/pages/Procesos.jsx
Normal file
385
src/pages/Procesos.jsx
Normal file
@@ -0,0 +1,385 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { fetchProcesamientoPedimentos } from '../api/procesos.ts';
|
||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||
const MICROSERVICE_URL = import.meta.env.VITE_EFC_MICROSERVICE_URL;
|
||||
|
||||
// Estado para loading de ejecución de servicio
|
||||
// y función para ejecutar el servicio según el tipo de proceso
|
||||
|
||||
|
||||
|
||||
export default function Procesos() {
|
||||
const [procesos, setProcesos] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [count, setCount] = useState(0);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(12);
|
||||
// Filtros
|
||||
const [pedimentoPedimentoFilter, setPedimentoPedimentoFilter] = useState('');
|
||||
const [estadoFilter, setEstadoFilter] = useState('');
|
||||
const [servicioFilter, setServicioFilter] = useState('');
|
||||
|
||||
// Estado para loading de ejecución de servicio
|
||||
const [executingId, setExecutingId] = useState(null);
|
||||
|
||||
// Dropdown state: id del proceso abierto o null
|
||||
const [openDropdownId, setOpenDropdownId] = useState(null);
|
||||
|
||||
|
||||
// Función para ejecutar el servicio según el tipo de proceso
|
||||
const handleEjecutarServicio = async (proc) => {
|
||||
setExecutingId(proc.id);
|
||||
let endpoint = '';
|
||||
// Determinar endpoint según el tipo de servicio
|
||||
switch (proc.servicio) {
|
||||
case 4: // Partidas
|
||||
endpoint = '/services/partidas';
|
||||
break;
|
||||
case 5: // Remesas
|
||||
endpoint = '/services/remesas';
|
||||
break;
|
||||
case 6: // Acuse
|
||||
endpoint = '/services/acuse';
|
||||
break;
|
||||
case 8: // Acuse Cove
|
||||
endpoint = '/services/acuseCove';
|
||||
break;
|
||||
default:
|
||||
alert('Servicio no soportado para ejecución directa.');
|
||||
setExecutingId(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem('access');
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
};
|
||||
const body = JSON.stringify({
|
||||
pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento,
|
||||
organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId,
|
||||
});
|
||||
const res = await fetch(`${MICROSERVICE_URL}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
if (!res.ok) throw new Error('Error al ejecutar el servicio');
|
||||
alert('Servicio ejecutado correctamente');
|
||||
setOpenDropdownId(null);
|
||||
} catch (err) {
|
||||
alert('Error al ejecutar el servicio: ' + (err instanceof Error ? err.message : String(err)));
|
||||
} finally {
|
||||
setExecutingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Cierra el dropdown si se hace click fuera
|
||||
useEffect(() => {
|
||||
if (openDropdownId === null) return;
|
||||
function handleClick(e) {
|
||||
const el = document.getElementById(`dropdown-acciones-${openDropdownId}`);
|
||||
if (el && !el.contains(e.target)) {
|
||||
setOpenDropdownId(null);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => document.removeEventListener('mousedown', handleClick);
|
||||
}, [openDropdownId]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchProcesos() {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const token = localStorage.getItem('access');
|
||||
// Construir query params
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', String(page));
|
||||
params.append('page_size', String(itemsPerPage));
|
||||
if (pedimentoPedimentoFilter) params.append('pedimento__pedimento', pedimentoPedimentoFilter);
|
||||
if (estadoFilter) params.append('estado', estadoFilter);
|
||||
if (servicioFilter) params.append('servicio', servicioFilter);
|
||||
// ...existing code...
|
||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
const res = await fetch(`${API_URL}/customs/procesamientopedimentos/?${params.toString()}`, { headers });
|
||||
if (!res.ok) throw new Error('Error al obtener procesamiento de pedimentos');
|
||||
const data = await res.json();
|
||||
setProcesos(data.results || []);
|
||||
setCount(data.count || 0);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
fetchProcesos();
|
||||
}, [page, itemsPerPage, pedimentoPedimentoFilter, estadoFilter, servicioFilter]);
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-50 min-h-screen">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<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 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards' }}>
|
||||
<div className="flex-shrink-0 bg-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">
|
||||
<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-3xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
||||
Procesos del Sistema
|
||||
</h1>
|
||||
<p className="text-lg text-blue-700/80 font-medium">Estado actual de los procesos de la agencia aduanal</p>
|
||||
</div>
|
||||
<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>
|
||||
<style>{`
|
||||
@keyframes bounce-slow {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
.animate-bounce-slow {
|
||||
animation: bounce-slow 2.2s infinite;
|
||||
}
|
||||
@keyframes fadein-slideup {
|
||||
0% { opacity: 0; transform: translateY(40px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.animate-fadein-slideup {
|
||||
animation: fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-8 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' }}>
|
||||
<h2 className="text-2xl font-bold text-blue-800 mb-6">Procesamiento de Pedimentos</h2>
|
||||
{/* Filtros */}
|
||||
<div className="mb-4 flex flex-wrap gap-4 items-end justify-between">
|
||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Pedimento</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pedimentoPedimentoFilter}
|
||||
onChange={e => setPedimentoPedimentoFilter(e.target.value)}
|
||||
placeholder="Buscar por pedimento..."
|
||||
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-gray-50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Estado</label>
|
||||
<select
|
||||
value={estadoFilter}
|
||||
onChange={e => setEstadoFilter(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-gray-50"
|
||||
>
|
||||
<option value="">Todos</option>
|
||||
<option value="1">En Espera</option>
|
||||
<option value="2">Procesando</option>
|
||||
<option value="3">Finalizado</option>
|
||||
<option value="4">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 min-w-[150px]">
|
||||
<label className="text-xs font-semibold text-gray-700 mb-1">Servicio</label>
|
||||
<select
|
||||
value={servicioFilter}
|
||||
onChange={e => setServicioFilter(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-gray-50"
|
||||
>
|
||||
<option value="">Todos</option>
|
||||
<option value="1">Estado de pedimento</option>
|
||||
<option value="2">Listado de pedimentos</option>
|
||||
<option value="3">Pedimento Completo</option>
|
||||
<option value="4">Pedimento Partidas</option>
|
||||
<option value="5">Pedimento Remesas</option>
|
||||
<option value="6">Acuse</option>
|
||||
<option value="7">EDocument</option>
|
||||
<option value="8">Cove</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* ...filtros anteriores... */}
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="text-center text-gray-500 py-8">Cargando procesos...</div>
|
||||
) : error ? (
|
||||
<div className="text-center text-danger-600 py-8">{error}</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<div style={{ minHeight: 'calc(6 * 56px)', maxHeight: 'calc(6 * 56px)', overflowY: 'auto', position: 'relative' }}>
|
||||
<table className="min-w-full divide-y divide-gray-200 rounded-lg overflow-hidden sticky text-xs">
|
||||
<thead className="bg-gradient-to-r from-gray-50 sticky top-0 z-20">
|
||||
<tr>
|
||||
<th className="px-2 py-2 text-center font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">ID</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Organización</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Estado</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Pedimento</th>
|
||||
<th className="px-2 py-2 text-left font-bold uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Servicio</th>
|
||||
<th className="px-2 py-2 text-center font-bold text-blue-700 uppercase tracking-wider border-b border-gray-200 whitespace-nowrap">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-100" style={{ position: 'relative', minHeight: 'calc(12 * 40px)' }}>
|
||||
{procesos.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="text-center py-8 text-gray-500">No hay registros</td>
|
||||
</tr>
|
||||
) : (
|
||||
procesos.map((proc) => (
|
||||
<tr key={proc.id} className="transition-all duration-200 hover:bg-blue-100 hover:shadow-lg">
|
||||
<td className="px-2 py-2 text-center align-middle whitespace-nowrap">{proc.id}</td>
|
||||
<td className="px-2 py-2 whitespace-nowrap align-middle">{proc.organizacion_name || '-'}</td>
|
||||
<td className="px-2 py-2 whitespace-nowrap align-middle">{
|
||||
proc.estado === 1 ? 'En Espera'
|
||||
: proc.estado === 2 ? 'Procesando'
|
||||
: proc.estado === 3 ? 'Finalizado'
|
||||
: proc.estado === 4 ? 'Error'
|
||||
: String(proc.estado)
|
||||
}</td>
|
||||
<td className="px-2 py-2 whitespace-nowrap align-middle">{
|
||||
typeof proc.pedimento === 'object' && proc.pedimento !== null
|
||||
? proc.pedimento.pedimento || JSON.stringify(proc.pedimento)
|
||||
: proc.pedimento
|
||||
}</td>
|
||||
<td className="px-2 py-2 whitespace-nowrap align-middle">{
|
||||
proc.servicio === 1 ? 'Estado de pedimento'
|
||||
: proc.servicio === 2 ? 'Listado de pedimentos'
|
||||
: proc.servicio === 3 ? 'Pedimento Completo'
|
||||
: proc.servicio === 4 ? 'Pedimento Partidas'
|
||||
: proc.servicio === 5 ? 'Pedimento Remesas'
|
||||
: proc.servicio === 6 ? 'Acuse'
|
||||
: proc.servicio === 7 ? 'EDocument'
|
||||
: proc.servicio === 8 ? 'Cove'
|
||||
: String(proc.servicio)
|
||||
}</td>
|
||||
<td className="px-2 py-2 text-center align-middle whitespace-nowrap">
|
||||
<div className="relative inline-block text-left" id={`dropdown-acciones-${proc.id}`}>
|
||||
<button
|
||||
className="inline-flex justify-center w-full rounded-md border border-gray-300 shadow-sm px-3 py-1 bg-white text-xs font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105"
|
||||
type="button"
|
||||
onClick={() => setOpenDropdownId(openDropdownId === proc.id ? null : proc.id)}
|
||||
>
|
||||
Acciones
|
||||
<svg className="ml-1 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /></svg>
|
||||
</button>
|
||||
{openDropdownId === proc.id && (
|
||||
<div className="absolute right-0 mt-2 w-32 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-20">
|
||||
<div className="py-1">
|
||||
<button
|
||||
className="block w-full text-left px-4 py-2 text-xs text-blue-700 hover:bg-blue-100 disabled:opacity-60"
|
||||
onClick={() => handleEjecutarServicio(proc)}
|
||||
disabled={executingId === proc.id}
|
||||
>
|
||||
{executingId === proc.id ? 'Ejecutando...' : 'Ejecutar Servicio'}
|
||||
</button>
|
||||
<button className="block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-blue-100">Pasar a espera</button>
|
||||
<button className="block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-blue-100">Editar</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* Paginación igual a Documents.jsx */}
|
||||
{count > 0 && (
|
||||
<div className="bg-white px-4 py-3 flex flex-col sm:flex-row items-center justify-between border-t border-gray-200">
|
||||
{(() => {
|
||||
const totalPages = Math.max(1, Math.ceil(count / itemsPerPage));
|
||||
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="itemsPerPage" className="text-xs text-gray-600 font-medium">Registros por página:</label>
|
||||
<select
|
||||
id="itemsPerPage"
|
||||
value={itemsPerPage}
|
||||
onChange={e => { setItemsPerPage(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"
|
||||
>
|
||||
{[5, 8, 12, 20, 50, 100].map(size => (
|
||||
<option key={size} value={size}>{size}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={e => { e.preventDefault(); setPage(1); }}
|
||||
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
|
||||
type="button"
|
||||
onClick={e => { e.preventDefault(); setPage(p => Math.max(1, p - 1)); }}
|
||||
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
|
||||
type="button"
|
||||
key={num}
|
||||
onClick={e => { e.preventDefault(); setPage(num); }}
|
||||
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
|
||||
type="button"
|
||||
onClick={e => { e.preventDefault(); setPage(p => p + 1); }}
|
||||
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
|
||||
type="button"
|
||||
onClick={e => { e.preventDefault(); setPage(totalPages); }}
|
||||
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>
|
||||
);
|
||||
}
|
||||
80
src/pages/Reportes.jsx
Normal file
80
src/pages/Reportes.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Reportes() {
|
||||
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>
|
||||
<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" />
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
316
src/pages/Reports.jsx
Normal file
316
src/pages/Reports.jsx
Normal file
@@ -0,0 +1,316 @@
|
||||
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' },
|
||||
];
|
||||
|
||||
// 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' },
|
||||
],
|
||||
};
|
||||
|
||||
const [activeTab, setActiveTab] = useState('pedimentos');
|
||||
const [nombreReporte, setNombreReporte] = useState('');
|
||||
const [columnas, setColumnas] = useState(['id', 'nombre']);
|
||||
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]
|
||||
);
|
||||
};
|
||||
|
||||
const handleGenerarReporte = (e) => {
|
||||
e.preventDefault();
|
||||
alert(`Generando reporte: ${nombreReporte}\nTipo: ${activeTab}\nColumnas: ${columnas.join(', ')}\nPedimento: ${pedimento}\nFecha: ${fechaInicio} a ${fechaFin}`);
|
||||
};
|
||||
|
||||
// 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="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">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
431
src/pages/Settings.jsx
Normal file
431
src/pages/Settings.jsx
Normal file
@@ -0,0 +1,431 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { getCurrentUser } from '../api/users.ts';
|
||||
|
||||
const Settings = () => {
|
||||
const [activeTab, setActiveTab] = useState('profile');
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Cargar información del usuario al montar el componente
|
||||
useEffect(() => {
|
||||
const loadUserData = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('access');
|
||||
if (token) {
|
||||
const userData = await getCurrentUser(token);
|
||||
setCurrentUser(userData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al cargar datos del usuario:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadUserData();
|
||||
}, []);
|
||||
|
||||
// Solo mostrar tabs permitidas si es importador
|
||||
const isImportador = typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true';
|
||||
const tabs = [
|
||||
{ id: 'profile', name: 'Perfil', icon: 'user' },
|
||||
{ id: 'organization', name: 'Organización', icon: 'building' },
|
||||
{ id: 'security', name: 'Seguridad', icon: 'shield' },
|
||||
{ id: 'notifications', name: 'Notificaciones', icon: 'bell' }
|
||||
].filter(tab =>
|
||||
isImportador
|
||||
? tab.id === 'profile' || tab.id === 'security' || tab.id === 'notifications'
|
||||
: true
|
||||
);
|
||||
|
||||
const getTabIcon = (iconType) => {
|
||||
const icons = {
|
||||
user: (
|
||||
<svg className="w-5 h-5" 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>
|
||||
),
|
||||
building: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
),
|
||||
shield: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
),
|
||||
bell: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-5-5v5zM4 4h5l-5 5v-5z" />
|
||||
</svg>
|
||||
)
|
||||
};
|
||||
return icons[iconType];
|
||||
};
|
||||
|
||||
const renderProfileTab = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Información Personal</h3>
|
||||
|
||||
{loading ? (
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-20 h-20 bg-gray-200 rounded-full"></div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 bg-gray-200 rounded w-32"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-24"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Avatar y información básica */}
|
||||
<div className="flex items-center space-x-6 mb-6">
|
||||
<div className="flex-shrink-0">
|
||||
{currentUser?.profile_picture ? (
|
||||
<img
|
||||
className="w-20 h-20 rounded-full object-cover"
|
||||
src={currentUser.profile_picture}
|
||||
alt="Avatar del usuario"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-20 h-20 bg-gray-300 rounded-full flex items-center justify-center">
|
||||
<svg className="w-10 h-10 text-gray-600" 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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold text-gray-900">
|
||||
{currentUser ? `${currentUser.first_name} ${currentUser.last_name}` : 'Usuario'}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500">
|
||||
{currentUser?.username || 'Sin username'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
ID: {currentUser?.id || 'Sin ID'}
|
||||
</p>
|
||||
<button className="mt-2 text-sm text-indigo-600 hover:text-indigo-500">
|
||||
Cambiar foto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Formulario de información */}
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nombre
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={currentUser?.first_name || ''}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Ingresa tu nombre"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Apellido
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={currentUser?.last_name || ''}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Ingresa tu apellido"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
defaultValue={currentUser?.email || ''}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="correo@ejemplo.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={currentUser?.username || ''}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="nombre_usuario"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{currentUser?.rfc && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
RFC
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={currentUser.rfc}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="XXXX000000XXX"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Organización ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={currentUser?.organizacion || ''}
|
||||
disabled
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-50 text-gray-500 cursor-not-allowed text-sm"
|
||||
placeholder="ID de organización"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-6 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
|
||||
>
|
||||
Guardar cambios
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderOrganizationTab = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Configuración de Organización</h3>
|
||||
<p className="text-sm text-gray-600 mb-6">
|
||||
Gestiona la configuración relacionada con tu organización.
|
||||
</p>
|
||||
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-yellow-800">Próximamente</h3>
|
||||
<div className="mt-2 text-sm text-yellow-700">
|
||||
<p>Las configuraciones de organización estarán disponibles pronto.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderSecurityTab = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Seguridad</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-2">Cambiar contraseña</h4>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Contraseña actual
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Ingresa tu contraseña actual"
|
||||
/>
|
||||
</div>
|
||||
<div></div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nueva contraseña
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Ingresa una nueva contraseña"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Confirmar nueva contraseña
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Confirma tu nueva contraseña"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
|
||||
>
|
||||
Actualizar contraseña
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderNotificationsTab = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Notificaciones</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-900">
|
||||
Notificaciones por email
|
||||
</label>
|
||||
<p className="text-sm text-gray-500">
|
||||
Recibir notificaciones importantes por correo electrónico
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-indigo-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<span className="translate-x-5 pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-900">
|
||||
Notificaciones de documentos
|
||||
</label>
|
||||
<p className="text-sm text-gray-500">
|
||||
Notificar cuando se suban o actualicen documentos
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-indigo-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<span className="translate-x-5 pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-900">
|
||||
Notificaciones del sistema
|
||||
</label>
|
||||
<p className="text-sm text-gray-500">
|
||||
Recibir actualizaciones del sistema y mantenimiento
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-gray-200 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<span className="translate-x-0 pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'profile':
|
||||
return renderProfileTab();
|
||||
case 'organization':
|
||||
return renderOrganizationTab();
|
||||
case 'security':
|
||||
return renderSecurityTab();
|
||||
case 'notifications':
|
||||
return renderNotificationsTab();
|
||||
default:
|
||||
return renderProfileTab();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Configuración</h1>
|
||||
<p className="mt-2 text-gray-600">
|
||||
Gestiona tu perfil, configuración de cuenta y preferencias.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="lg:grid lg:grid-cols-12 lg:gap-x-5">
|
||||
{/* Sidebar de navegación */}
|
||||
<aside className="py-6 px-2 sm:px-6 lg:py-0 lg:px-0 lg:col-span-3">
|
||||
<nav className="space-y-1">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`${
|
||||
activeTab === tab.id
|
||||
? 'bg-indigo-50 border-indigo-500 text-indigo-700'
|
||||
: 'border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900'
|
||||
} group border-l-4 px-3 py-2 flex items-center text-sm font-medium w-full text-left`}
|
||||
>
|
||||
<span
|
||||
className={`${
|
||||
activeTab === tab.id
|
||||
? 'text-indigo-500'
|
||||
: 'text-gray-400 group-hover:text-gray-500'
|
||||
} flex-shrink-0 -ml-1 mr-3 h-6 w-6`}
|
||||
>
|
||||
{getTabIcon(tab.icon)}
|
||||
</span>
|
||||
<span className="truncate">{tab.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* Contenido principal */}
|
||||
<div className="space-y-6 sm:px-6 lg:px-0 lg:col-span-9">
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
{renderTabContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
425
src/pages/SettingsNew.jsx
Normal file
425
src/pages/SettingsNew.jsx
Normal file
@@ -0,0 +1,425 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { getCurrentUser } from '../api/users.js';
|
||||
|
||||
const Settings = () => {
|
||||
const [activeTab, setActiveTab] = useState('profile');
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Cargar información del usuario al montar el componente
|
||||
useEffect(() => {
|
||||
const loadUserData = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('access');
|
||||
if (token) {
|
||||
const userData = await getCurrentUser(token);
|
||||
setCurrentUser(userData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al cargar datos del usuario:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadUserData();
|
||||
}, []);
|
||||
|
||||
const tabs = [
|
||||
{ id: 'profile', name: 'Perfil', icon: 'user' },
|
||||
{ id: 'organization', name: 'Organización', icon: 'building' },
|
||||
{ id: 'security', name: 'Seguridad', icon: 'shield' },
|
||||
{ id: 'notifications', name: 'Notificaciones', icon: 'bell' }
|
||||
];
|
||||
|
||||
const getTabIcon = (iconType) => {
|
||||
const icons = {
|
||||
user: (
|
||||
<svg className="w-5 h-5" 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>
|
||||
),
|
||||
building: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
),
|
||||
shield: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
),
|
||||
bell: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-5-5v5zM4 4h5l-5 5v-5z" />
|
||||
</svg>
|
||||
)
|
||||
};
|
||||
return icons[iconType];
|
||||
};
|
||||
|
||||
const renderProfileTab = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Información Personal</h3>
|
||||
|
||||
{loading ? (
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-20 h-20 bg-gray-200 rounded-full"></div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 bg-gray-200 rounded w-32"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-24"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Avatar y información básica */}
|
||||
<div className="flex items-center space-x-6 mb-6">
|
||||
<div className="flex-shrink-0">
|
||||
{currentUser?.profile_picture ? (
|
||||
<img
|
||||
className="w-20 h-20 rounded-full object-cover"
|
||||
src={currentUser.profile_picture}
|
||||
alt="Avatar del usuario"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-20 h-20 bg-gray-300 rounded-full flex items-center justify-center">
|
||||
<svg className="w-10 h-10 text-gray-600" 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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold text-gray-900">
|
||||
{currentUser ? `${currentUser.first_name} ${currentUser.last_name}` : 'Usuario'}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500">
|
||||
{currentUser?.username || 'Sin username'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
ID: {currentUser?.id || 'Sin ID'}
|
||||
</p>
|
||||
<button className="mt-2 text-sm text-indigo-600 hover:text-indigo-500">
|
||||
Cambiar foto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Formulario de información */}
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nombre
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={currentUser?.first_name || ''}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Ingresa tu nombre"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Apellido
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={currentUser?.last_name || ''}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Ingresa tu apellido"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
defaultValue={currentUser?.email || ''}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="correo@ejemplo.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={currentUser?.username || ''}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="nombre_usuario"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{currentUser?.rfc && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
RFC
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={currentUser.rfc}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="XXXX000000XXX"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Organización ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={currentUser?.organizacion || ''}
|
||||
disabled
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-50 text-gray-500 cursor-not-allowed text-sm"
|
||||
placeholder="ID de organización"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-6 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
|
||||
>
|
||||
Guardar cambios
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderOrganizationTab = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Configuración de Organización</h3>
|
||||
<p className="text-sm text-gray-600 mb-6">
|
||||
Gestiona la configuración relacionada con tu organización.
|
||||
</p>
|
||||
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-yellow-800">Próximamente</h3>
|
||||
<div className="mt-2 text-sm text-yellow-700">
|
||||
<p>Las configuraciones de organización estarán disponibles pronto.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderSecurityTab = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Seguridad</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-2">Cambiar contraseña</h4>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Contraseña actual
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Ingresa tu contraseña actual"
|
||||
/>
|
||||
</div>
|
||||
<div></div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nueva contraseña
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Ingresa una nueva contraseña"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Confirmar nueva contraseña
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Confirma tu nueva contraseña"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
|
||||
>
|
||||
Actualizar contraseña
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderNotificationsTab = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Notificaciones</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-900">
|
||||
Notificaciones por email
|
||||
</label>
|
||||
<p className="text-sm text-gray-500">
|
||||
Recibir notificaciones importantes por correo electrónico
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-indigo-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<span className="translate-x-5 pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-900">
|
||||
Notificaciones de documentos
|
||||
</label>
|
||||
<p className="text-sm text-gray-500">
|
||||
Notificar cuando se suban o actualicen documentos
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-indigo-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<span className="translate-x-5 pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-900">
|
||||
Notificaciones del sistema
|
||||
</label>
|
||||
<p className="text-sm text-gray-500">
|
||||
Recibir actualizaciones del sistema y mantenimiento
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-gray-200 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<span className="translate-x-0 pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'profile':
|
||||
return renderProfileTab();
|
||||
case 'organization':
|
||||
return renderOrganizationTab();
|
||||
case 'security':
|
||||
return renderSecurityTab();
|
||||
case 'notifications':
|
||||
return renderNotificationsTab();
|
||||
default:
|
||||
return renderProfileTab();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Configuración</h1>
|
||||
<p className="mt-2 text-gray-600">
|
||||
Gestiona tu perfil, configuración de cuenta y preferencias.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="lg:grid lg:grid-cols-12 lg:gap-x-5">
|
||||
{/* Sidebar de navegación */}
|
||||
<aside className="py-6 px-2 sm:px-6 lg:py-0 lg:px-0 lg:col-span-3">
|
||||
<nav className="space-y-1">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`${
|
||||
activeTab === tab.id
|
||||
? 'bg-indigo-50 border-indigo-500 text-indigo-700'
|
||||
: 'border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900'
|
||||
} group border-l-4 px-3 py-2 flex items-center text-sm font-medium w-full text-left`}
|
||||
>
|
||||
<span
|
||||
className={`${
|
||||
activeTab === tab.id
|
||||
? 'text-indigo-500'
|
||||
: 'text-gray-400 group-hover:text-gray-500'
|
||||
} flex-shrink-0 -ml-1 mr-3 h-6 w-6`}
|
||||
>
|
||||
{getTabIcon(tab.icon)}
|
||||
</span>
|
||||
<span className="truncate">{tab.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* Contenido principal */}
|
||||
<div className="space-y-6 sm:px-6 lg:px-0 lg:col-span-9">
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
{renderTabContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
326
src/pages/TableroAlmacenamiento.jsx
Normal file
326
src/pages/TableroAlmacenamiento.jsx
Normal file
@@ -0,0 +1,326 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Line, Pie, Doughnut, Bar } from 'react-chartjs-2';
|
||||
|
||||
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
ArcElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
} from 'chart.js';
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, BarElement, ArcElement, Title, Tooltip, Legend);
|
||||
|
||||
export default function TableroAlmacenamiento() {
|
||||
// Estado para la tabla de documentos y la opción seleccionada
|
||||
const [selectedMetric, setSelectedMetric] = useState('');
|
||||
const [documentos, setDocumentos] = useState([
|
||||
{ nombre: 'Factura_123.pdf', tipo: 'Factura', ext: 'PDF' },
|
||||
{ nombre: 'Pedimento_456.xml', tipo: 'Pedimento', ext: 'XML' },
|
||||
{ nombre: 'Manifiesto_789.docx', tipo: 'Manifiesto', ext: 'DOCX' },
|
||||
]);
|
||||
|
||||
// Por ahora solo cambia el estado seleccionado, no fetch
|
||||
const handleMetricClick = (metric) => {
|
||||
setSelectedMetric(metric);
|
||||
};
|
||||
|
||||
// Datos simulados para las nuevas gráficas y KPIs
|
||||
const tiposArchivos = [
|
||||
{ tipo: 'PDF', espacio: 220 },
|
||||
{ tipo: 'XML', espacio: 120 },
|
||||
{ tipo: 'DOCX', espacio: 80 },
|
||||
{ tipo: 'JPG', espacio: 60 },
|
||||
{ tipo: 'Otros', espacio: 32 },
|
||||
];
|
||||
const topArchivos = [
|
||||
{ nombre: 'Factura_123.pdf', size: 2.5 },
|
||||
{ nombre: 'Reporte_2024.pdf', size: 2.1 },
|
||||
{ nombre: 'Pedimento_456.xml', size: 1.8 },
|
||||
{ nombre: 'Manifiesto_789.docx', size: 1.2 },
|
||||
{ nombre: 'Imagen_001.jpg', size: 1.0 },
|
||||
];
|
||||
const espacioTotal = 1024; // GB
|
||||
const espacioOcupado = 512; // GB
|
||||
const espacioLibre = espacioTotal - espacioOcupado;
|
||||
const usuarios = [
|
||||
{ nombre: 'Juan', docs: 120 },
|
||||
{ nombre: 'Ana', docs: 90 },
|
||||
{ nombre: 'Luis', docs: 70 },
|
||||
{ nombre: 'Sofía', docs: 60 },
|
||||
{ nombre: 'Carlos', docs: 40 },
|
||||
];
|
||||
const docsEsteMes = 45;
|
||||
const docsEliminados = 7;
|
||||
const usuariosActivos = 4;
|
||||
const porcentajeUsado = Math.round((espacioOcupado / espacioTotal) * 100);
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-50 min-h-screen flex flex-col">
|
||||
{/* Header animado */}
|
||||
<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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight mb-1 flex items-center gap-2">
|
||||
Uso de Almacenamiento
|
||||
</h1>
|
||||
<p className="text-lg text-blue-700/80 font-medium">Visualiza y analiza el uso de almacenamiento de la plataforma</p>
|
||||
</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>
|
||||
<style>{`
|
||||
@keyframes bounce-slow {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
.animate-bounce-slow {
|
||||
animation: bounce-slow 2.2s infinite;
|
||||
}
|
||||
@keyframes fadein-slideup {
|
||||
0% { opacity: 0; transform: translateY(40px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.animate-fadein-slideup {
|
||||
animation: fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.05s forwards;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filtros */}
|
||||
<div className="max-w-7xl mx-auto w-full mb-8 flex flex-col md:flex-row gap-4 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.12s forwards' }}>
|
||||
<select className="border border-gray-300 rounded-lg px-4 py-2 text-sm bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1" defaultValue="">
|
||||
<option value="">Organización</option>
|
||||
<option value="org1">Organización 1</option>
|
||||
<option value="org2">Organización 2</option>
|
||||
</select>
|
||||
<select className="border border-gray-300 rounded-lg px-4 py-2 text-sm bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1" defaultValue="">
|
||||
<option value="">Importador</option>
|
||||
<option value="imp1">Importador 1</option>
|
||||
<option value="imp2">Importador 2</option>
|
||||
</select>
|
||||
<input type="date" className="border border-gray-300 rounded-lg px-4 py-2 text-sm bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1" />
|
||||
<select className="border border-gray-300 rounded-lg px-4 py-2 text-sm bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1" defaultValue="">
|
||||
<option value="">Año</option>
|
||||
<option value="2022">2022</option>
|
||||
<option value="2023">2023</option>
|
||||
<option value="2024">2024</option>
|
||||
<option value="2025">2025</option>
|
||||
</select>
|
||||
<select className="border border-gray-300 rounded-lg px-4 py-2 text-sm bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1" defaultValue="">
|
||||
<option value="">Mes</option>
|
||||
<option value="01">Enero</option>
|
||||
<option value="02">Febrero</option>
|
||||
<option value="03">Marzo</option>
|
||||
<option value="04">Abril</option>
|
||||
<option value="05">Mayo</option>
|
||||
<option value="06">Junio</option>
|
||||
<option value="07">Julio</option>
|
||||
<option value="08">Agosto</option>
|
||||
<option value="09">Septiembre</option>
|
||||
<option value="10">Octubre</option>
|
||||
<option value="11">Noviembre</option>
|
||||
<option value="12">Diciembre</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Cards y KPIs */}
|
||||
<div className="max-w-7xl mx-auto w-full grid grid-cols-1 md:grid-cols-6 gap-6 mb-8 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.18s forwards' }}>
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6 flex flex-col items-center shadow hover:scale-105 transition-transform duration-200">
|
||||
<span className="text-3xl font-bold text-blue-800 mb-2">1,234</span>
|
||||
<span className="text-sm font-semibold text-blue-700">Total de Pedimentos</span>
|
||||
</div>
|
||||
<div className="bg-indigo-50 border border-indigo-200 rounded-xl p-6 flex flex-col items-center shadow hover:scale-105 transition-transform duration-200">
|
||||
<span className="text-3xl font-bold text-indigo-800 mb-2">8,765</span>
|
||||
<span className="text-sm font-semibold text-indigo-700">Total de Documentos</span>
|
||||
</div>
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-6 flex flex-col items-center shadow hover:scale-105 transition-transform duration-200">
|
||||
<span className="text-3xl font-bold text-green-800 mb-2">{espacioOcupado} GB</span>
|
||||
<span className="text-sm font-semibold text-green-700">Espacio Utilizado</span>
|
||||
</div>
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-6 flex flex-col items-center shadow hover:scale-105 transition-transform duration-200">
|
||||
<span className="text-3xl font-bold text-yellow-800 mb-2">2.5 GB</span>
|
||||
<span className="text-sm font-semibold text-yellow-700">Archivo más grande</span>
|
||||
</div>
|
||||
<div className="bg-pink-50 border border-pink-200 rounded-xl p-6 flex flex-col items-center shadow hover:scale-105 transition-transform duration-200">
|
||||
<span className="text-3xl font-bold text-pink-800 mb-2">120 MB</span>
|
||||
<span className="text-sm font-semibold text-pink-700">Tamaño promedio</span>
|
||||
</div>
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-xl p-6 flex flex-col items-center shadow hover:scale-105 transition-transform duration-200">
|
||||
<span className="text-3xl font-bold text-gray-800 mb-2">{espacioLibre} GB</span>
|
||||
<span className="text-sm font-semibold text-gray-700">Espacio Libre</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto w-full grid grid-cols-1 md:grid-cols-4 gap-6 mb-8 animate-fadein-slideup opacity-0">
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 flex flex-col items-center shadow">
|
||||
<span className="text-2xl font-bold text-blue-700 mb-1">{porcentajeUsado}%</span>
|
||||
<span className="text-xs text-gray-600">% Espacio Usado</span>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 flex flex-col items-center shadow">
|
||||
<span className="text-2xl font-bold text-green-700 mb-1">{docsEsteMes}</span>
|
||||
<span className="text-xs text-gray-600">Docs subidos este mes</span>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 flex flex-col items-center shadow">
|
||||
<span className="text-2xl font-bold text-red-700 mb-1">{docsEliminados}</span>
|
||||
<span className="text-xs text-gray-600">Docs eliminados este mes</span>
|
||||
</div>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 flex flex-col items-center shadow">
|
||||
<span className="text-2xl font-bold text-indigo-700 mb-1">{usuariosActivos}</span>
|
||||
<span className="text-xs text-gray-600">Usuarios activos este mes</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gráficas */}
|
||||
<div className="max-w-7xl mx-auto w-full grid grid-cols-1 md:grid-cols-3 gap-8 animate-fadein-slideup opacity-0" style={{ animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.22s forwards' }}>
|
||||
{/* Gráfica 1: Espacio utilizado a lo largo del tiempo */}
|
||||
<div className="bg-white border border-gray-200 rounded-2xl p-6 shadow flex flex-col">
|
||||
<h2 className="text-lg font-bold text-blue-800 mb-4">Espacio utilizado a lo largo del tiempo</h2>
|
||||
<Line
|
||||
data={{
|
||||
labels: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Espacio Utilizado (GB)',
|
||||
data: [100, 150, 200, 250, 300, 400, 512],
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59,130,246,0.1)',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
title: { display: false },
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: 'Mes' } },
|
||||
y: { title: { display: true, text: 'GB' } },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Gráfica 2: Distribución de tipos de archivo */}
|
||||
<div className="bg-white border border-gray-200 rounded-2xl p-6 shadow flex flex-col">
|
||||
<h2 className="text-lg font-bold text-purple-800 mb-4">Distribución por tipo de archivo</h2>
|
||||
<Pie
|
||||
data={{
|
||||
labels: tiposArchivos.map(t => t.tipo),
|
||||
datasets: [
|
||||
{
|
||||
data: tiposArchivos.map(t => t.espacio),
|
||||
backgroundColor: ['#3b82f6', '#6366f1', '#f59e42', '#10b981', '#f472b6'],
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
plugins: {
|
||||
legend: { display: true, position: 'bottom' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Gráfica 3: Espacio ocupado vs libre (donut) */}
|
||||
<div className="bg-white border border-gray-200 rounded-2xl p-6 shadow flex flex-col">
|
||||
<h2 className="text-lg font-bold text-green-800 mb-4">Espacio ocupado vs libre</h2>
|
||||
<Doughnut
|
||||
data={{
|
||||
labels: ['Ocupado', 'Libre'],
|
||||
datasets: [
|
||||
{
|
||||
data: [espacioOcupado, espacioLibre],
|
||||
backgroundColor: ['#3b82f6', '#d1fae5'],
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
cutout: '70%',
|
||||
plugins: {
|
||||
legend: { display: true, position: 'bottom' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Gráficas adicionales */}
|
||||
<div className="max-w-7xl mx-auto w-full grid grid-cols-1 md:grid-cols-2 gap-8 mt-8 animate-fadein-slideup opacity-0">
|
||||
{/* Top archivos más grandes */}
|
||||
<div className="bg-white border border-gray-200 rounded-2xl p-6 shadow flex flex-col">
|
||||
<h2 className="text-lg font-bold text-yellow-800 mb-4">Top 5 archivos más grandes</h2>
|
||||
<Bar
|
||||
data={{
|
||||
labels: topArchivos.map(a => a.nombre),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Tamaño (GB)',
|
||||
data: topArchivos.map(a => a.size),
|
||||
backgroundColor: '#f59e42',
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
indexAxis: 'y',
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: 'Tamaño (GB)' } },
|
||||
y: { title: { display: false } },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Documentos subidos por usuario */}
|
||||
<div className="bg-white border border-gray-200 rounded-2xl p-6 shadow flex flex-col">
|
||||
<h2 className="text-lg font-bold text-indigo-800 mb-4">Documentos subidos por usuario</h2>
|
||||
<Bar
|
||||
data={{
|
||||
labels: usuarios.map(u => u.nombre),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Documentos',
|
||||
data: usuarios.map(u => u.docs),
|
||||
backgroundColor: '#6366f1',
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: false } },
|
||||
y: { title: { display: true, text: 'Documentos' } },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
src/pages/Test.jsx
Normal file
20
src/pages/Test.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Test() {
|
||||
console.log('🟢 Test component loaded');
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '50px',
|
||||
backgroundColor: '#ff0000',
|
||||
color: 'white',
|
||||
fontSize: '30px',
|
||||
textAlign: 'center',
|
||||
minHeight: '100vh'
|
||||
}}>
|
||||
<h1>🚨 TEST PAGE 🚨</h1>
|
||||
<p>Si ves esto, React funciona!</p>
|
||||
<p>Hora: {new Date().toLocaleTimeString()}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1243
src/pages/Users.jsx
Normal file
1243
src/pages/Users.jsx
Normal file
File diff suppressed because it is too large
Load Diff
723
src/pages/UsersNew.jsx
Normal file
723
src/pages/UsersNew.jsx
Normal file
@@ -0,0 +1,723 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { fetchUsers, createUser, updateUser, deleteUser } from '../api/users.js';
|
||||
import { useNotification } from '../context/NotificationContext';
|
||||
|
||||
const initialForm = {
|
||||
username: '',
|
||||
email: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
export default function Users() {
|
||||
const [users, setUsers] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [form, setForm] = useState(initialForm);
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [userToDelete, setUserToDelete] = useState(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const { showMessage } = useNotification();
|
||||
|
||||
const token = localStorage.getItem('access');
|
||||
|
||||
const loadUsers = () => {
|
||||
setLoading(true);
|
||||
fetchUsers(token)
|
||||
.then(data => {
|
||||
setUsers(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.message === 'SESSION_EXPIRED') {
|
||||
localStorage.removeItem('access');
|
||||
localStorage.removeItem('refresh');
|
||||
showMessage('Tu sesión ha expirado, por favor inicia sesión de nuevo.', 'error');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
} else {
|
||||
setError(err.message);
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
// eslint-disable-next-line
|
||||
}, [showMessage]);
|
||||
|
||||
const handleChange = e => {
|
||||
setForm({ ...form, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const handleSubmit = async e => {
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
try {
|
||||
if (editingId) {
|
||||
await updateUser(token, editingId, form);
|
||||
showMessage('Usuario actualizado exitosamente', 'success');
|
||||
setShowEditModal(false);
|
||||
} else {
|
||||
await createUser(token, form);
|
||||
showMessage('Usuario creado exitosamente', 'success');
|
||||
setShowCreateModal(false);
|
||||
}
|
||||
setForm(initialForm);
|
||||
setEditingId(null);
|
||||
loadUsers();
|
||||
} catch (err) {
|
||||
showMessage(err.message, 'error');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = user => {
|
||||
setForm({
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
password: '',
|
||||
});
|
||||
setEditingId(user.id);
|
||||
setShowEditModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (user) => {
|
||||
setUserToDelete(user);
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!userToDelete) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await deleteUser(token, userToDelete.id);
|
||||
showMessage('Usuario eliminado exitosamente', 'success');
|
||||
setShowDeleteModal(false);
|
||||
setUserToDelete(null);
|
||||
loadUsers();
|
||||
} catch (err) {
|
||||
showMessage(err.message, 'error');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setForm(initialForm);
|
||||
setEditingId(null);
|
||||
setShowCreateModal(false);
|
||||
setShowEditModal(false);
|
||||
setShowDeleteModal(false);
|
||||
setUserToDelete(null);
|
||||
};
|
||||
|
||||
// Función para filtrar usuarios
|
||||
const filteredUsers = users.filter(user =>
|
||||
user.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(user.first_name && user.first_name.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||||
(user.last_name && user.last_name.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
);
|
||||
|
||||
// Función para obtener el badge de estado
|
||||
const getStatusBadge = () => {
|
||||
return 'bg-green-100 text-green-800';
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="h-20 bg-gray-200 rounded"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800">Error al cargar usuarios</h3>
|
||||
<div className="mt-2 text-sm text-red-700">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Usuarios</h1>
|
||||
<p className="text-gray-600">Gestiona y supervisa los usuarios registrados en el sistema.</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div className="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Total Usuarios</dt>
|
||||
<dd className="text-lg font-medium text-gray-900">{users.length}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div className="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-6 w-6 text-green-600" 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" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Activos</dt>
|
||||
<dd className="text-lg font-medium text-gray-900">{users.length}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div className="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-6 w-6 text-purple-600" 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>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Con Perfil Completo</dt>
|
||||
<dd className="text-lg font-medium text-gray-900">
|
||||
{users.filter(u => u.first_name && u.last_name).length}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div className="p-5">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-6 w-6 text-orange-600" 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>
|
||||
</div>
|
||||
<div className="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">Última Semana</dt>
|
||||
<dd className="text-lg font-medium text-gray-900">
|
||||
{users.filter(u => u.id % 3 === 0).length}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search and Actions */}
|
||||
<div className="bg-white shadow rounded-lg mb-6">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" 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>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md"
|
||||
placeholder="Buscar usuarios..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 sm:mt-0 sm:ml-4">
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
Nuevo Usuario
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Usuario
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Email
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Estado
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Nombre Completo
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
ID
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Acciones</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredUsers.map((user) => (
|
||||
<tr key={user.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 h-10 w-10">
|
||||
{user.profile_picture ? (
|
||||
<img
|
||||
className="h-10 w-10 rounded-full object-cover"
|
||||
src={user.profile_picture}
|
||||
alt="Avatar"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-gray-600" 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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="text-sm font-medium text-gray-900">{user.username}</div>
|
||||
<div className="text-sm text-gray-500">ID: {user.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{user.email}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusBadge()}`}>
|
||||
Activo
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{user.first_name || user.last_name ?
|
||||
`${user.first_name} ${user.last_name}`.trim() :
|
||||
<span className="text-gray-400 italic">Sin nombre</span>
|
||||
}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
#{user.id}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button
|
||||
onClick={() => handleEdit(user)}
|
||||
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
||||
>
|
||||
Editar
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteClick(user)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Eliminar
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Empty state */}
|
||||
{filteredUsers.length === 0 && !loading && (
|
||||
<div className="text-center py-12">
|
||||
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900">No se encontraron usuarios</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
{searchTerm ? 'Intenta con otros términos de búsqueda.' : 'Comienza agregando un nuevo usuario.'}
|
||||
</p>
|
||||
{!searchTerm && (
|
||||
<div className="mt-6">
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<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 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
Nuevo Usuario
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modales */}
|
||||
{/* Modal Crear Usuario */}
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div className="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
|
||||
<div className="mt-3">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">Crear Nuevo Usuario</h3>
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<svg className="w-6 h-6" 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>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nombre de usuario *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value={form.username}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="nombre_usuario"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="usuario@ejemplo.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nombre
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="first_name"
|
||||
value={form.first_name}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Nombre"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Apellido
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="last_name"
|
||||
value={form.last_name}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Apellido"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Contraseña *
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Contraseña del usuario"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50 flex items-center"
|
||||
>
|
||||
{submitting && (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
)}
|
||||
{submitting ? 'Creando...' : 'Crear Usuario'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal Editar Usuario */}
|
||||
{showEditModal && (
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div className="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
|
||||
<div className="mt-3">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">Editar Usuario</h3>
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<svg className="w-6 h-6" 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>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nombre de usuario *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value={form.username}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="nombre_usuario"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="usuario@ejemplo.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nombre
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="first_name"
|
||||
value={form.first_name}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Nombre"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Apellido
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="last_name"
|
||||
value={form.last_name}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Apellido"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nueva contraseña
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm transition-colors"
|
||||
placeholder="Dejar vacío para mantener actual"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Deja este campo vacío si no deseas cambiar la contraseña
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50 flex items-center"
|
||||
>
|
||||
{submitting && (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
)}
|
||||
{submitting ? 'Actualizando...' : 'Actualizar Usuario'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal Eliminar Usuario */}
|
||||
{showDeleteModal && userToDelete && (
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div className="relative top-20 mx-auto p-5 border w-11/12 md:w-96 shadow-lg rounded-md bg-white">
|
||||
<div className="mt-3 text-center">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
|
||||
<svg className="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Eliminar Usuario</h3>
|
||||
<div className="mt-2 px-7 py-3">
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
¿Estás seguro que deseas eliminar al usuario <strong>{userToDelete.username}</strong>?
|
||||
</p>
|
||||
<div className="bg-gray-50 rounded-md p-3 mb-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
{userToDelete.profile_picture ? (
|
||||
<img
|
||||
className="h-10 w-10 rounded-full object-cover"
|
||||
src={userToDelete.profile_picture}
|
||||
alt="Avatar"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
|
||||
<svg className="h-6 w-6 text-gray-600" 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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-3 text-left">
|
||||
<p className="text-sm font-medium text-gray-900">{userToDelete.username}</p>
|
||||
<p className="text-sm text-gray-500">{userToDelete.email}</p>
|
||||
{(userToDelete.first_name || userToDelete.last_name) && (
|
||||
<p className="text-xs text-gray-400">
|
||||
{`${userToDelete.first_name} ${userToDelete.last_name}`.trim()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-red-600">
|
||||
Esta acción no se puede deshacer.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center space-x-3 pt-4">
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteConfirm}
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors disabled:opacity-50 flex items-center"
|
||||
>
|
||||
{submitting && (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
)}
|
||||
{submitting ? 'Eliminando...' : 'Eliminar Usuario'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
19
src/pages/Vucem.jsx
Normal file
19
src/pages/Vucem.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Vucem() {
|
||||
return (
|
||||
<div className="p-8 min-h-screen bg-gradient-to-br from-blue-50 to-blue-100 flex flex-col items-center justify-center">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 max-w-xl w-full text-center">
|
||||
<h1 className="text-3xl font-extrabold text-blue-800 mb-4">Vucem</h1>
|
||||
<p className="text-gray-600 mb-6">Esta es la vista de integración con VUCEM. Aquí podrás consultar, gestionar o integrar funcionalidades relacionadas con la Ventanilla Única de Comercio Exterior Mexicana.</p>
|
||||
<div className="flex flex-col gap-4 items-center">
|
||||
<svg className="w-16 h-16 text-blue-400 mx-auto" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 12h8M12 8v8" />
|
||||
</svg>
|
||||
<span className="text-blue-700 font-semibold">Próximamente podrás ver información y acciones de VUCEM aquí.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user