Se agregaron datos del ticker 2025-08-046
This commit is contained in:
@@ -24,16 +24,35 @@ const API_URL = import.meta.env.VITE_EFC_API_URL;
|
|||||||
// Obtiene la lista de documentos (pedimentos)
|
// Obtiene la lista de documentos (pedimentos)
|
||||||
export interface PedimentosFilters {
|
export interface PedimentosFilters {
|
||||||
search?: string;
|
search?: string;
|
||||||
|
id?: string;
|
||||||
|
documentos_count?: number;
|
||||||
|
documentos_peso_total?: number;
|
||||||
pedimento?: string;
|
pedimento?: string;
|
||||||
existe_expediente?: string | boolean;
|
pedimento_app?: string;
|
||||||
alerta?: string | boolean;
|
|
||||||
contribuyente?: string;
|
|
||||||
curp_apoderado?: string;
|
|
||||||
fecha_pago?: string;
|
|
||||||
patente?: string;
|
patente?: string;
|
||||||
aduana?: string;
|
aduana?: string;
|
||||||
tipo_operacion?: string;
|
regimen?: string;
|
||||||
clave_pedimento?: string;
|
clave_pedimento?: string;
|
||||||
|
fecha_inicio?: string;
|
||||||
|
fecha_fin?: string;
|
||||||
|
fecha_pago?: string;
|
||||||
|
alerta?: string | boolean;
|
||||||
|
agente_aduanal?: string;
|
||||||
|
curp_apoderado?: string;
|
||||||
|
importe_total?: string;
|
||||||
|
saldo_disponible?: string;
|
||||||
|
importe_pedimento?: string;
|
||||||
|
existe_expediente?: string | boolean;
|
||||||
|
remesas?: string | boolean;
|
||||||
|
numero_partidas?: number;
|
||||||
|
numero_operacion?: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
organizacion?: string;
|
||||||
|
tipo_operacion?: string | number;
|
||||||
|
contribuyente?: string;
|
||||||
|
numero_edocs?: number;
|
||||||
|
numero_coves?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchDocuments(
|
export async function fetchDocuments(
|
||||||
|
|||||||
8
src/assets/animate-fade-in-fast.css
Normal file
8
src/assets/animate-fade-in-fast.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
@keyframes fadeInFast {
|
||||||
|
from { opacity: 0; transform: translateY(16px) scale(0.98); }
|
||||||
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-fast {
|
||||||
|
animation: fadeInFast 0.12s cubic-bezier(0.4,0,0.2,1);
|
||||||
|
}
|
||||||
@@ -183,6 +183,16 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) {
|
|||||||
{
|
{
|
||||||
title: 'Acceso a Usuarios',
|
title: 'Acceso a Usuarios',
|
||||||
items: [
|
items: [
|
||||||
|
// Botón Importadores como primer elemento
|
||||||
|
{
|
||||||
|
name: 'Importadores',
|
||||||
|
path: '/importers',
|
||||||
|
icon: (
|
||||||
|
<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 01-8 0M12 11v10m-6 0h12a2 2 0 002-2v-5a2 2 0 00-2-2H6a2 2 0 00-2 2v5a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
},
|
||||||
...(
|
...(
|
||||||
isImportador
|
isImportador
|
||||||
? []
|
? []
|
||||||
|
|||||||
@@ -60,8 +60,10 @@ function RegistrosCargadosModal({ open, onClose, registros }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Procesar datastage (adaptado para mostrar registros cargados)
|
// Procesar datastage (adaptado para mostrar registros cargados)
|
||||||
async function procesarDatastage(item, setDatastages, setSuccess, setError, setRegistrosCargados, setShowRegistrosModal) {
|
// Recibe setEnProcesoId como argumento para manejar el estado desde el componente
|
||||||
|
async function procesarDatastage(item, setDatastages, setSuccess, setError, setRegistrosCargados, setShowRegistrosModal, setEnProcesoId) {
|
||||||
try {
|
try {
|
||||||
|
setEnProcesoId(item.id);
|
||||||
const url = `${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/${item.id}/procesar/`;
|
const url = `${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/${item.id}/procesar/`;
|
||||||
const body = {
|
const body = {
|
||||||
organizacion: item.organizacion,
|
organizacion: item.organizacion,
|
||||||
@@ -88,6 +90,8 @@ async function procesarDatastage(item, setDatastages, setSuccess, setError, setR
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError('No se pudo procesar el datastage');
|
setError('No se pudo procesar el datastage');
|
||||||
|
} finally {
|
||||||
|
setEnProcesoId(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Descarga autenticada de archivos datastage
|
// Descarga autenticada de archivos datastage
|
||||||
@@ -128,6 +132,8 @@ export default function Datastage() {
|
|||||||
// Animación header
|
// Animación header
|
||||||
const [showAnimation, setShowAnimation] = useState(false);
|
const [showAnimation, setShowAnimation] = useState(false);
|
||||||
const [hasAnimated, setHasAnimated] = useState(false);
|
const [hasAnimated, setHasAnimated] = useState(false);
|
||||||
|
// Estado para mostrar mensaje "En proceso" por cada datastage
|
||||||
|
const [enProcesoId, setEnProcesoId] = useState(null);
|
||||||
useLayoutEffect(() => { setShowAnimation(true); }, []);
|
useLayoutEffect(() => { setShowAnimation(true); }, []);
|
||||||
useEffect(() => { if (showAnimation && !hasAnimated) setTimeout(() => setHasAnimated(true), 800); }, [showAnimation, hasAnimated]);
|
useEffect(() => { if (showAnimation && !hasAnimated) setTimeout(() => setHasAnimated(true), 800); }, [showAnimation, hasAnimated]);
|
||||||
|
|
||||||
@@ -180,46 +186,6 @@ export default function Datastage() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Editar
|
|
||||||
const handleEdit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const fd = new FormData();
|
|
||||||
fd.append('contribuyente', form.contribuyente);
|
|
||||||
if (form.archivo) fd.append('archivo', form.archivo);
|
|
||||||
await patchFormDataWithAuth(`${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/${editingId}/`, fd);
|
|
||||||
setForm({ archivo: null, contribuyente: '' });
|
|
||||||
setEditingId(null);
|
|
||||||
setShowEditModal(false);
|
|
||||||
setSuccess('Datastage actualizado exitosamente');
|
|
||||||
setShowSuccessModal(true);
|
|
||||||
load();
|
|
||||||
} catch (e) {
|
|
||||||
setError(e.message);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Eliminar
|
|
||||||
const handleDelete = async () => {
|
|
||||||
if (!deleteId) return;
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
await deleteDatastage(deleteId);
|
|
||||||
if (selected && selected.id === deleteId) setSelected(null);
|
|
||||||
setShowDeleteModal(false);
|
|
||||||
setSuccess('Datastage eliminado exitosamente');
|
|
||||||
setShowSuccessModal(true);
|
|
||||||
load();
|
|
||||||
} catch (e) {
|
|
||||||
setError(e.message);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Abrir modal de edición
|
// Abrir modal de edición
|
||||||
const openEditModal = (item) => {
|
const openEditModal = (item) => {
|
||||||
setForm({ archivo: null, contribuyente: item.contribuyente });
|
setForm({ archivo: null, contribuyente: item.contribuyente });
|
||||||
@@ -403,14 +369,18 @@ export default function Datastage() {
|
|||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
<svg className="w-5 h-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>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => procesarDatastage(item, setDatastages, setSuccess, setError, setRegistrosCargados, setShowRegistrosModal)}
|
onClick={() => procesarDatastage(item, setDatastages, setSuccess, setError, setRegistrosCargados, setShowRegistrosModal, setEnProcesoId)}
|
||||||
className={`inline-flex items-center justify-center w-9 h-9 rounded-lg border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-1 ${item.procesado ? 'bg-gray-100 border-gray-200 cursor-not-allowed opacity-50' : 'bg-green-50 border-green-200 hover:bg-green-100 hover:border-green-300 focus:ring-green-500 cursor-pointer'}`}
|
className={`inline-flex items-center justify-center w-24 h-9 rounded-lg border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-1 text-sm font-semibold ${item.procesado ? 'bg-gray-100 border-gray-200 cursor-not-allowed opacity-50' : enProcesoId === item.id ? 'bg-yellow-50 border-yellow-200 text-yellow-700 cursor-wait' : 'bg-green-50 border-green-200 hover:bg-green-100 hover:border-green-300 focus:ring-green-500 cursor-pointer'}`}
|
||||||
title={item.procesado ? 'Ya procesado' : 'Procesar'}
|
title={item.procesado ? 'Ya procesado' : enProcesoId === item.id ? 'En proceso' : 'Procesar'}
|
||||||
disabled={item.procesado}
|
disabled={item.procesado || enProcesoId === item.id}
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 24 24">
|
{item.procesado ? (
|
||||||
<path d="M8 5v14l11-7z"/>
|
<span className="flex items-center gap-1"><svg className="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>Procesado</span>
|
||||||
</svg>
|
) : enProcesoId === item.id ? (
|
||||||
|
<span className="flex items-center gap-1"><svg className="w-5 h-5 animate-spin text-yellow-500" fill="none" stroke="currentColor" 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-8v8z"></path></svg>En proceso...</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-1"><svg className="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>Procesar</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -480,14 +450,18 @@ export default function Datastage() {
|
|||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
<svg className="w-5 h-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>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => procesarDatastage(item, setDatastages, setSuccess, setError, setRegistrosCargados, setShowRegistrosModal)}
|
onClick={() => procesarDatastage(item, setDatastages, setSuccess, setError, setRegistrosCargados, setShowRegistrosModal, setEnProcesoId)}
|
||||||
className={`inline-flex items-center justify-center w-9 h-9 rounded-lg border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-1 ${item.procesado ? 'bg-gray-100 border-gray-200 cursor-not-allowed opacity-50' : 'bg-green-50 border-green-200 hover:bg-green-100 hover:border-green-300 focus:ring-green-500 cursor-pointer'}`}
|
className={`inline-flex items-center justify-center w-24 h-9 rounded-lg border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-1 text-sm font-semibold ${item.procesado ? 'bg-gray-100 border-gray-200 cursor-not-allowed opacity-50' : enProcesoId === item.id ? 'bg-yellow-50 border-yellow-200 text-yellow-700 cursor-wait' : 'bg-green-50 border-green-200 hover:bg-green-100 hover:border-green-300 focus:ring-green-500 cursor-pointer'}`}
|
||||||
title={item.procesado ? 'Ya procesado' : 'Procesar'}
|
title={item.procesado ? 'Ya procesado' : enProcesoId === item.id ? 'En proceso' : 'Procesar'}
|
||||||
disabled={item.procesado}
|
disabled={item.procesado || enProcesoId === item.id}
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 24 24">
|
{item.procesado ? (
|
||||||
<path d="M8 5v14l11-7z"/>
|
<span className="flex items-center gap-1"><svg className="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>Procesado</span>
|
||||||
</svg>
|
) : enProcesoId === item.id ? (
|
||||||
|
<span className="flex items-center gap-1"><svg className="w-5 h-5 animate-spin text-yellow-500" fill="none" stroke="currentColor" 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-8v8z"></path></svg>En proceso...</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-1"><svg className="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>Procesar</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -497,44 +471,141 @@ export default function Datastage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modales */}
|
{/* Modales */}
|
||||||
{/* Modal de creación */}
|
|
||||||
{showCreateModal && (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-40">
|
|
||||||
<form onSubmit={handleCreate} className="bg-white rounded-xl shadow-2xl border border-blue-200 p-8 max-w-sm w-full flex flex-col animate-fade-in">
|
|
||||||
<h2 className="text-xl font-bold text-blue-700 mb-4">Nuevo Datastage</h2>
|
|
||||||
{error && <div className="text-red-500 mb-2">{error}</div>}
|
|
||||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Archivo (.zip)</label>
|
|
||||||
<input type="file" accept=".zip" className="mb-3 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 transition-all" onChange={e => setForm(f => ({ ...f, archivo: e.target.files[0] }))} required />
|
|
||||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Contribuyente</label>
|
|
||||||
<input className="mb-3 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 transition-all" value={form.contribuyente} onChange={e => setForm(f => ({ ...f, contribuyente: e.target.value }))} required />
|
|
||||||
<div className="flex gap-2 mt-2 justify-end">
|
|
||||||
<button type="button" onClick={() => setShowCreateModal(false)} className="px-4 py-2 bg-gray-300 text-gray-800 rounded-lg font-semibold shadow hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-400">Cancelar</button>
|
|
||||||
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded-lg font-semibold shadow hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400">Crear</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Modal de edición */}
|
{/* Modal de creación - estilo Users/Importers */}
|
||||||
{showEditModal && (
|
{showCreateModal && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-40">
|
<div className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm overflow-y-auto h-full w-full z-50 flex items-center justify-center p-4">
|
||||||
<form onSubmit={handleEdit} className="bg-white rounded-xl shadow-2xl border border-yellow-200 p-8 max-w-sm w-full flex flex-col animate-fade-in">
|
<form onSubmit={handleCreate} className="relative mx-auto w-full max-w-xl bg-white rounded-2xl shadow-2xl transform transition-all duration-300 animate-in slide-in-from-bottom-4">
|
||||||
<h2 className="text-xl font-bold text-yellow-700 mb-4">Editar Datastage</h2>
|
{/* Header */}
|
||||||
{error && <div className="text-red-500 mb-2">{error}</div>}
|
<div className="bg-gradient-to-r from-blue-700 to-blue-900 rounded-t-2xl p-4 text-white border-b-2 border-blue-500">
|
||||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Archivo (.zip)</label>
|
<div className="flex items-center justify-between">
|
||||||
<input type="file" accept=".zip" className="mb-3 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500 bg-gray-50 transition-all" onChange={e => setForm(f => ({ ...f, archivo: e.target.files[0] }))} />
|
<div className="flex items-center space-x-3">
|
||||||
<label className="block text-xs font-semibold text-gray-700 mb-1">Contribuyente</label>
|
<div className="bg-blue-500 bg-opacity-30 rounded-xl p-2 border border-blue-400 border-opacity-30">
|
||||||
<input className="mb-3 w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500 bg-gray-50 transition-all" value={form.contribuyente} onChange={e => setForm(f => ({ ...f, contribuyente: e.target.value }))} required />
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<div className="flex gap-2 mt-2 justify-end">
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
<button type="button" onClick={handleCancelEdit} className="px-4 py-2 bg-gray-300 text-gray-800 rounded-lg font-semibold shadow hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-400">Cancelar</button>
|
</svg>
|
||||||
<button type="submit" className="px-4 py-2 bg-yellow-600 text-white rounded-lg font-semibold shadow hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-yellow-400">Actualizar</button>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
</form>
|
<h3 className="text-lg font-semibold tracking-wide">Nuevo Datastage</h3>
|
||||||
</div>
|
<p className="text-blue-200 text-xs font-medium">Carga un archivo .zip y asigna un contribuyente</p>
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={() => setShowCreateModal(false)} className="text-blue-100 hover:text-white hover:bg-blue-600 transition-colors p-2 hover:bg-opacity-50 rounded-lg border border-blue-500 border-opacity-30">
|
||||||
|
<svg className="w-5 h-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>
|
||||||
|
</div>
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
{error && <div className="text-red-500 mb-2">{error}</div>}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-700 mb-1">Archivo (.zip)</label>
|
||||||
|
<input type="file" accept=".zip" className="w-full border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-slate-900 placeholder-slate-400 text-sm" onChange={e => setForm(f => ({ ...f, archivo: e.target.files[0] }))} required />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-700 mb-1">Contribuyente</label>
|
||||||
|
<input className="w-full border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white text-slate-900 placeholder-slate-400 text-sm" value={form.contribuyente} onChange={e => setForm(f => ({ ...f, contribuyente: e.target.value }))} required />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col sm:flex-row justify-end space-y-2 sm:space-y-0 sm:space-x-3 pt-4 border-t border-slate-200">
|
||||||
|
<button type="button" onClick={() => setShowCreateModal(false)} className="w-full sm:w-auto px-6 py-2 border border-slate-300 rounded-md shadow-sm text-sm font-semibold text-slate-700 bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-all duration-200">Cancelar</button>
|
||||||
|
<button type="submit" className="w-full sm:w-auto px-6 py-2 border border-transparent rounded-md shadow-lg text-sm font-semibold text-white bg-gradient-to-r from-blue-700 to-blue-900 hover:from-blue-800 hover:to-blue-950 focus:ring-blue-500 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 flex items-center justify-center space-x-2">Crear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Modal de confirmación para eliminar */}
|
|
||||||
<ConfirmModal open={showDeleteModal} onClose={() => setShowDeleteModal(false)} onConfirm={handleDelete} message="¿Seguro que deseas eliminar este datastage?" confirmText="Eliminar" cancelText="Cancelar" />
|
{/* Modal de edición - estilo Users/Importers */}
|
||||||
|
{showEditModal && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm overflow-y-auto h-full w-full z-50 flex items-center justify-center p-4">
|
||||||
|
<form onSubmit={handleEdit} className="relative mx-auto w-full max-w-xl bg-white rounded-2xl shadow-2xl transform transition-all duration-300 animate-in slide-in-from-bottom-4">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-gradient-to-r from-yellow-600 to-yellow-800 rounded-t-2xl p-4 text-white border-b-2 border-yellow-400">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="bg-yellow-500 bg-opacity-30 rounded-xl p-2 border border-yellow-400 border-opacity-30">
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15.232 5.232l3.536 3.536M9 13l6-6 3.536 3.536a2 2 0 010 2.828l-7.072 7.072a2 2 0 01-2.828 0l-3.536-3.536a2 2 0 010-2.828l7.072-7.072z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold tracking-wide">Editar Datastage</h3>
|
||||||
|
<p className="text-yellow-200 text-xs font-medium">Actualiza el archivo o el contribuyente</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={handleCancelEdit} className="text-yellow-100 hover:text-white hover:bg-yellow-600 transition-colors p-2 hover:bg-opacity-50 rounded-lg border border-yellow-400 border-opacity-30">
|
||||||
|
<svg className="w-5 h-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>
|
||||||
|
</div>
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
{error && <div className="text-red-500 mb-2">{error}</div>}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-700 mb-1">Archivo (.zip)</label>
|
||||||
|
<input type="file" accept=".zip" className="w-full border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500 transition-all duration-200 bg-white text-slate-900 placeholder-slate-400 text-sm" onChange={e => setForm(f => ({ ...f, archivo: e.target.files[0] }))} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-700 mb-1">Contribuyente</label>
|
||||||
|
<input className="w-full border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-yellow-500 focus:border-yellow-500 transition-all duration-200 bg-white text-slate-900 placeholder-slate-400 text-sm" value={form.contribuyente} onChange={e => setForm(f => ({ ...f, contribuyente: e.target.value }))} required />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col sm:flex-row justify-end space-y-2 sm:space-y-0 sm:space-x-3 pt-4 border-t border-slate-200">
|
||||||
|
<button type="button" onClick={handleCancelEdit} className="w-full sm:w-auto px-6 py-2 border border-slate-300 rounded-md shadow-sm text-sm font-semibold text-slate-700 bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-all duration-200">Cancelar</button>
|
||||||
|
<button type="submit" className="w-full sm:w-auto px-6 py-2 border border-transparent rounded-md shadow-lg text-sm font-semibold text-white bg-gradient-to-r from-yellow-600 to-yellow-800 hover:from-yellow-700 hover:to-yellow-900 focus:ring-yellow-500 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 flex items-center justify-center space-x-2">Actualizar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Modal de eliminación - estilo Users/Importers */}
|
||||||
|
{showDeleteModal && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm overflow-y-auto h-full w-full z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="relative mx-auto w-full max-w-md bg-white rounded-2xl shadow-2xl transform transition-all duration-300 animate-in slide-in-from-bottom-4">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-gradient-to-r from-red-700 to-red-900 rounded-t-2xl p-4 text-white border-b-2 border-red-500">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="bg-red-500 bg-opacity-30 rounded-xl p-2 border border-red-400 border-opacity-30">
|
||||||
|
<svg className="w-5 h-5" 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>
|
||||||
|
<h3 className="text-lg font-semibold tracking-wide">Eliminar Datastage</h3>
|
||||||
|
<p className="text-red-200 text-xs font-medium">Esta acción no se puede deshacer.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={() => setShowDeleteModal(false)} className="text-red-100 hover:text-white hover:bg-red-600 transition-colors p-2 hover:bg-opacity-50 rounded-lg border border-red-500 border-opacity-30">
|
||||||
|
<svg className="w-5 h-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>
|
||||||
|
</div>
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6 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 este datastage?</h3>
|
||||||
|
<p className="text-sm text-gray-500 mb-4">¿Seguro que deseas eliminar este datastage? Esta acción no se puede deshacer.</p>
|
||||||
|
<div className="flex justify-center space-x-3 pt-4">
|
||||||
|
<button type="button" onClick={() => setShowDeleteModal(false)} className="px-6 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-all duration-200">Cancelar</button>
|
||||||
|
<button type="button" onClick={handleDelete} className="px-6 py-2 border border-transparent rounded-md shadow-lg text-sm font-semibold text-white bg-gradient-to-r from-red-700 to-red-900 hover:from-red-800 hover:to-red-950 focus:ring-red-500 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 flex items-center justify-center space-x-2">Eliminar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Modal de detalle */}
|
{/* Modal de detalle */}
|
||||||
{showDetailModal && selected && (
|
{showDetailModal && selected && (
|
||||||
|
|||||||
@@ -419,8 +419,11 @@ export default function Documents() {
|
|||||||
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Contribuyente</th>
|
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Contribuyente</th>
|
||||||
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">CURP Apoderado</th>
|
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">CURP Apoderado</th>
|
||||||
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Partidas</th>
|
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Partidas</th>
|
||||||
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Saldo disponible</th>
|
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Fecha de Carga</th>
|
||||||
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Importe pedimento</th>
|
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Tipo Operacion</th>
|
||||||
|
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Clave</th>
|
||||||
|
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">No. Archivos</th>
|
||||||
|
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Peso Total</th>
|
||||||
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Expediente</th>
|
<th className="px-4 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider border-b border-gray-200">Expediente</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -462,8 +465,11 @@ export default function Documents() {
|
|||||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700 max-w-xs truncate" title={ped.contribuyente}>{ped.contribuyente}</td>
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700 max-w-xs truncate" title={ped.contribuyente}>{ped.contribuyente}</td>
|
||||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700">{ped.curp_apoderado}</td>
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-700">{ped.curp_apoderado}</td>
|
||||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">{ped.numero_partidas}</td>
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">{ped.numero_partidas}</td>
|
||||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">${ped.saldo_disponible}</td>
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">{ped.created_at ? ped.created_at.slice(0, 10) : ''}</td>
|
||||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">${ped.importe_pedimento_app}</td>
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">{ped.tipo_operacion}</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">{ped.clave_pedimento}</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">{ped.documentos_count}</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900 font-semibold">{ped.documentos_peso_total}</td>
|
||||||
<td className="px-4 py-4 whitespace-nowrap">
|
<td className="px-4 py-4 whitespace-nowrap">
|
||||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold ${
|
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold ${
|
||||||
ped.existe_expediente
|
ped.existe_expediente
|
||||||
@@ -568,17 +574,29 @@ export default function Documents() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-1 gap-2">
|
<div className="grid grid-cols-1 gap-2">
|
||||||
|
<div className="flex items-center justify-between bg-green-50 rounded-lg p-2">
|
||||||
|
<span className="text-sm font-medium text-green-700">Tipo Operacion</span>
|
||||||
|
<span className="text-sm font-bold text-green-800">{ped.tipo_operacion}</span>
|
||||||
|
</div>
|
||||||
<div className="flex items-center justify-between bg-green-50 rounded-lg p-2">
|
<div className="flex items-center justify-between bg-green-50 rounded-lg p-2">
|
||||||
<span className="text-sm font-medium text-green-700">Partidas</span>
|
<span className="text-sm font-medium text-green-700">Partidas</span>
|
||||||
<span className="text-sm font-bold text-green-800">${ped.numero_partidas}</span>
|
<span className="text-sm font-bold text-green-800">{ped.numero_partidas}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between bg-blue-50 rounded-lg p-2">
|
<div className="flex items-center justify-between bg-blue-50 rounded-lg p-2">
|
||||||
<span className="text-sm font-medium text-blue-700">Saldo disponible:</span>
|
<span className="text-sm font-medium text-blue-700">Fecha de Carga</span>
|
||||||
<span className="text-sm font-bold text-blue-800">${ped.saldo_disponible}</span>
|
<span className="text-sm font-bold text-blue-800">{ped.created_at ? ped.created_at.slice(0, 10) : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between bg-gray-50 rounded-lg p-2">
|
<div className="flex items-center justify-between bg-gray-50 rounded-lg p-2">
|
||||||
<span className="text-sm font-medium text-gray-700">Importe pedimento:</span>
|
<span className="text-sm font-medium text-gray-700">Clave</span>
|
||||||
<span className="text-sm font-bold text-gray-800">${ped.importe_pedimento_app}</span>
|
<span className="text-sm font-bold text-gray-800">{ped.clave_pedimento}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between bg-gray-50 rounded-lg p-2">
|
||||||
|
<span className="text-sm font-medium text-gray-700">No. Archivos</span>
|
||||||
|
<span className="text-sm font-bold text-gray-800"></span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between bg-gray-50 rounded-lg p-2">
|
||||||
|
<span className="text-sm font-medium text-gray-700">Peso Total</span>
|
||||||
|
<span className="text-sm font-bold text-gray-800">Peso total</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,309 +1,456 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import '../assets/animate-fade-in-fast.css';
|
||||||
|
import React, { useState, useEffect, useLayoutEffect, useRef } from 'react';
|
||||||
|
// Endpoint de credenciales VUCEM
|
||||||
|
|
||||||
|
|
||||||
|
import { fetchWithAuth } from '../fetchWithAuth';
|
||||||
|
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||||
|
|
||||||
|
// Animación fade-in/slide-up para bloques (igual que Expedientes)
|
||||||
|
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-importers')) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'fadein-slideup-importers';
|
||||||
|
style.innerHTML = fadeInSlideUp;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
const VUCEM_CREDENTIALS_URL = `${API_URL}/vucem/vucem/`;
|
||||||
export default function Importers() {
|
export default function Importers() {
|
||||||
|
const focusKeeperRef = useRef(null);
|
||||||
const [importers, setImporters] = useState([]);
|
const [importers, setImporters] = useState([]);
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [modalMode, setModalMode] = useState('create'); // 'create' | 'edit' | 'view' | 'delete'
|
||||||
|
const [modalData, setModalData] = useState({ rfc: '', nombre: '', organizacion: '' });
|
||||||
|
const [modalLoading, setModalLoading] = useState(false);
|
||||||
|
const [editId, setEditId] = useState(null);
|
||||||
|
const [errorMsg, setErrorMsg] = useState('');
|
||||||
|
const openViewModal = (importer) => {
|
||||||
|
setModalMode('view');
|
||||||
|
setModalData({ rfc: importer.rfc, nombre: importer.nombre, organizacion: importer.organizacion });
|
||||||
|
setModalOpen(true);
|
||||||
|
};
|
||||||
|
const openEditModal = (importer) => {
|
||||||
|
setModalMode('edit');
|
||||||
|
setModalData({ rfc: importer.rfc, nombre: importer.nombre, organizacion: importer.organizacion });
|
||||||
|
setEditId(importer.rfc);
|
||||||
|
setErrorMsg('');
|
||||||
|
setModalOpen(true);
|
||||||
|
};
|
||||||
|
const closeModal = () => {
|
||||||
|
setModalOpen(false);
|
||||||
|
setModalData({ rfc: '', nombre: '', organizacion: '' });
|
||||||
|
setEditId(null);
|
||||||
|
setErrorMsg('');
|
||||||
|
};
|
||||||
|
const handleModalChange = e => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setModalData(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
const openDeleteModal = (importer) => {
|
||||||
|
setModalMode('delete');
|
||||||
|
setModalData({ rfc: importer.rfc, nombre: importer.nombre, organizacion: importer.organizacion });
|
||||||
|
setModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
setModalLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetchWithAuth(`${API_URL}/customs/importadores/${modalData.rfc}/`, { method: 'DELETE' });
|
||||||
|
if (!res.ok) throw new Error('Error al eliminar importador');
|
||||||
|
// Refrescar lista
|
||||||
|
const res2 = await fetchWithAuth(`${API_URL}/customs/importadores/`);
|
||||||
|
if (!res2.ok) throw new Error('Error al obtener importadores');
|
||||||
|
const data = await res2.json();
|
||||||
|
setImporters(Array.isArray(data) ? data : []);
|
||||||
|
closeModal();
|
||||||
|
} catch {
|
||||||
|
setErrorMsg('Error al eliminar importador');
|
||||||
|
} finally {
|
||||||
|
setModalLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
|
const [showAnimation, setShowAnimation] = useState(false);
|
||||||
|
const [hasAnimated, setHasAnimated] = useState(false);
|
||||||
|
|
||||||
// Datos dummy para mostrar
|
useLayoutEffect(() => { setShowAnimation(true); }, []);
|
||||||
const dummyImporters = [
|
useEffect(() => {
|
||||||
{
|
if (showAnimation && !hasAnimated) {
|
||||||
id: 1,
|
const timeout = setTimeout(() => {
|
||||||
name: 'Importadora ABC S.A.',
|
setHasAnimated(true);
|
||||||
rfc: 'ABC123456789',
|
setShowAnimation(false);
|
||||||
email: 'contacto@abc.com',
|
}, 700);
|
||||||
status: 'Activo',
|
return () => clearTimeout(timeout);
|
||||||
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
|
|
||||||
}
|
}
|
||||||
];
|
}, [showAnimation, hasAnimated]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Simular carga de datos
|
setLoading(true);
|
||||||
const timer = setTimeout(() => {
|
fetchWithAuth(`${API_URL}/customs/importadores/`)
|
||||||
setImporters(dummyImporters);
|
.then(async res => {
|
||||||
setLoading(false);
|
if (!res.ok) throw new Error('Error al obtener importadores');
|
||||||
}, 1000);
|
const data = await res.json();
|
||||||
|
setImporters(Array.isArray(data) ? data : []);
|
||||||
return () => clearTimeout(timer);
|
})
|
||||||
|
.catch(() => setImporters([]))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filteredImporters = importers.filter(importer =>
|
const filteredImporters = importers.filter(importer =>
|
||||||
importer.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
(importer.nombre || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
importer.rfc.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
(importer.rfc || '').toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
importer.email.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cálculos de paginación
|
// Paginación
|
||||||
const totalImporters = filteredImporters.length;
|
const totalImporters = filteredImporters.length;
|
||||||
const totalPages = Math.ceil(totalImporters / itemsPerPage);
|
const totalPages = Math.ceil(totalImporters / itemsPerPage) || 1;
|
||||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
const endIndex = startIndex + itemsPerPage;
|
const endIndex = startIndex + itemsPerPage;
|
||||||
const currentImporters = filteredImporters.slice(startIndex, endIndex);
|
const currentImporters = filteredImporters.slice(startIndex, endIndex);
|
||||||
|
|
||||||
// Reset página cuando cambia el filtro
|
useEffect(() => { setCurrentPage(1); }, [searchTerm]);
|
||||||
useEffect(() => {
|
|
||||||
setCurrentPage(1);
|
|
||||||
}, [searchTerm]);
|
|
||||||
|
|
||||||
const handlePageChange = (page) => {
|
const handlePageChange = (page, e) => {
|
||||||
|
if (e && typeof e.preventDefault === 'function') e.preventDefault();
|
||||||
|
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
|
||||||
|
if (page < 1 || page > totalPages || page === currentPage) return;
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
|
if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
useLayoutEffect(() => { if (focusKeeperRef.current) focusKeeperRef.current.focus(); }, [currentPage]);
|
||||||
const handleItemsPerPageChange = (newItemsPerPage) => {
|
const handleItemsPerPageChange = (newItemsPerPage) => { setItemsPerPage(newItemsPerPage); setCurrentPage(1); };
|
||||||
setItemsPerPage(newItemsPerPage);
|
// No hay status real en el endpoint, así que lo dejamos azul
|
||||||
setCurrentPage(1); // Reset a la primera página
|
const getStatusBadge = () => 'bg-blue-100 text-blue-800 border border-blue-200';
|
||||||
};
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 p-6">
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 p-4 sm:p-6 lg:p-8">
|
||||||
|
<div 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">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Header */}
|
{/* Header animado y decorativo */}
|
||||||
<div className="mb-8">
|
<div className={
|
||||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6">
|
"mb-6 sm:mb-8 relative overflow-hidden rounded-3xl shadow-2xl bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 p-4 sm:p-8 flex flex-col sm:flex-row items-start sm:items-center gap-4 sm:gap-6" +
|
||||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-navy-900 to-navy-700 bg-clip-text text-transparent mb-2">
|
(showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '')
|
||||||
Importadores
|
}
|
||||||
|
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-white/20 backdrop-blur-sm rounded-full p-3 sm:p-4 shadow-lg animate-bounce-slow">
|
||||||
|
<svg className="h-8 w-8 sm:h-10 sm:w-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="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 className="flex-1 min-w-0">
|
||||||
|
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-white tracking-tight mb-1 flex flex-col sm:flex-row sm:items-center gap-2">
|
||||||
|
<span>Importadores</span>
|
||||||
|
{totalImporters > 0 && (
|
||||||
|
<span className="inline-block bg-white/20 backdrop-blur-sm text-white text-xs sm:text-sm font-semibold px-3 py-1 rounded-full shadow-lg animate-fade-in">
|
||||||
|
{totalImporters} registros
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600">Gestiona y supervisa las empresas importadoras registradas en el sistema.</p>
|
<p className="text-sm sm:text-lg text-blue-100 font-medium leading-relaxed">Gestiona y supervisa las empresas importadoras registradas en el sistema.</p>
|
||||||
|
</div>
|
||||||
|
{/* Efectos decorativos de fondo modernos */}
|
||||||
|
<div className="absolute -top-10 -right-10 opacity-20 pointer-events-none select-none">
|
||||||
|
<div className="w-32 h-32 bg-white/10 rounded-full blur-xl"></div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute -bottom-6 -left-6 opacity-15 pointer-events-none select-none">
|
||||||
|
<div className="w-24 h-24 bg-white/10 rounded-full blur-lg"></div>
|
||||||
|
</div>
|
||||||
|
{/* Partículas flotantes */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
|
<div className="absolute top-1/4 left-1/4 w-2 h-2 bg-white/30 rounded-full animate-ping"></div>
|
||||||
|
<div className="absolute top-3/4 right-1/3 w-1 h-1 bg-white/40 rounded-full animate-pulse"></div>
|
||||||
|
<div className="absolute top-1/2 right-1/4 w-3 h-3 bg-white/20 rounded-full animate-bounce"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<style>{`
|
||||||
|
@keyframes bounce-slow {
|
||||||
|
0%, 100% { transform: translateY(0) scale(1); }
|
||||||
|
50% { transform: translateY(-8px) scale(1.05); }
|
||||||
|
}
|
||||||
|
.animate-bounce-slow {
|
||||||
|
animation: bounce-slow 3s infinite;
|
||||||
|
}
|
||||||
|
@keyframes fade-in {
|
||||||
|
from { opacity: 0; transform: scale(0.9) translateY(10px); }
|
||||||
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
|
}
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fade-in 0.8s ease-out;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Filtros y acciones */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
<div className={
|
||||||
<div className="bg-white overflow-hidden shadow-xl rounded-xl border border-gray-200">
|
"bg-white shadow-2xl rounded-3xl border border-gray-100 overflow-hidden w-full" +
|
||||||
<div className="p-6">
|
(showAnimation && !hasAnimated ? ' animate-fadein-slideup opacity-0' : '')
|
||||||
<div className="flex items-center">
|
}
|
||||||
<div className="flex-shrink-0">
|
style={showAnimation && !hasAnimated ? { animation: 'fadein-slideup 0.7s cubic-bezier(0.22,1,0.36,1) 0.15s forwards' } : undefined}>
|
||||||
<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">
|
<div className="px-4 sm:px-6 py-4 sm:py-6 border-b border-gray-200 bg-gradient-to-r from-gray-50 to-blue-50/30">
|
||||||
<svg className="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="mb-4 sm:mb-6">
|
||||||
<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" />
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-3">
|
||||||
</svg>
|
<h3 className="text-sm font-semibold text-gray-800 flex items-center">
|
||||||
</div>
|
<svg className="w-4 h-4 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</div>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.414A1 1 0 013 6.707V4z" />
|
||||||
<div className="ml-5 w-0 flex-1">
|
</svg>
|
||||||
<dl>
|
Filtros de búsqueda
|
||||||
<dt className="text-sm font-medium text-gray-500 truncate">Total Importadores</dt>
|
</h3>
|
||||||
<dd className="text-2xl font-bold text-navy-900">{importers.length}</dd>
|
<button
|
||||||
</dl>
|
type="button"
|
||||||
</div>
|
onClick={() => { setModalMode('create'); setModalData({ rfc: '', nombre: '', organizacion: '' }); setErrorMsg(''); setModalOpen(true); }}
|
||||||
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-semibold text-xs sm:text-sm shadow-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
|
</svg>
|
||||||
|
Crear importador
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4 w-full">
|
||||||
</div>
|
<div className="flex flex-col">
|
||||||
|
<label className="text-xs font-semibold text-gray-700 mb-1.5">Buscar</label>
|
||||||
<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
|
<input
|
||||||
type="text"
|
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}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={e => setSearchTerm(e.target.value)}
|
||||||
|
placeholder="Buscar importador, RFC, email..."
|
||||||
|
className="w-full border border-gray-300 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white shadow-sm transition-all duration-200 hover:shadow-md"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Tabla de importadores */}
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-x-auto w-full">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200 text-xs sm:text-sm">
|
||||||
<thead className="bg-gradient-to-r from-gray-50 to-gray-100">
|
<thead className="bg-gradient-to-r from-gray-50 to-gray-100">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">RFC</th>
|
||||||
Importador
|
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Nombre</th>
|
||||||
</th>
|
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Organización</th>
|
||||||
<th scope="col" className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
|
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Creado</th>
|
||||||
RFC
|
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Actualizado</th>
|
||||||
</th>
|
<th className="relative px-6 py-4"><span className="sr-only">Acciones</span></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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{currentImporters.map((importer, index) => (
|
{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'}`}>
|
<tr key={importer.rfc} className={`hover:bg-blue-50 transition-colors duration-200 ${index % 2 === 0 ? 'bg-white' : 'bg-blue-50'} text-xs sm:text-sm`}>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-4 sm:px-6 py-3 whitespace-nowrap font-medium text-gray-900 break-all max-w-[120px] sm:max-w-none">{importer.rfc}</td>
|
||||||
<div className="flex items-center">
|
<td className="px-4 sm:px-6 py-3 whitespace-nowrap text-gray-900 break-all max-w-[120px] sm:max-w-none">{importer.nombre || <span className="italic text-gray-400">Sin nombre</span>}</td>
|
||||||
<div className="flex-shrink-0 h-12 w-12">
|
<td className="px-4 sm:px-6 py-3 whitespace-nowrap text-xs text-gray-700 font-mono break-all max-w-[120px] sm:max-w-none">{importer.organizacion}</td>
|
||||||
<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">
|
<td className="px-4 sm:px-6 py-3 whitespace-nowrap text-xs text-gray-500">{importer.created_at ? new Date(importer.created_at).toLocaleString() : ''}</td>
|
||||||
<svg className="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<td className="px-4 sm:px-6 py-3 whitespace-nowrap text-xs text-gray-500">{importer.updated_at ? new Date(importer.updated_at).toLocaleString() : ''}</td>
|
||||||
<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" />
|
<td className="px-4 sm:px-6 py-3 whitespace-nowrap text-right font-medium">
|
||||||
</svg>
|
<div className="flex items-center justify-end space-x-1 sm:space-x-2">
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center justify-center p-2 rounded-full bg-blue-50 hover:bg-blue-100 text-blue-700 transition-all focus:outline-none"
|
||||||
|
title="Ver"
|
||||||
|
aria-label="Ver"
|
||||||
|
onClick={() => openViewModal(importer)}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center justify-center p-2 rounded-full bg-yellow-50 hover:bg-yellow-100 text-yellow-700 transition-all focus:outline-none"
|
||||||
|
title="Editar"
|
||||||
|
aria-label="Editar"
|
||||||
|
onClick={() => openEditModal(importer)}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5h2m-1 0v14m-7-7h14" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center justify-center p-2 rounded-full bg-red-50 hover:bg-red-100 text-red-700 transition-all focus:outline-none"
|
||||||
|
title="Eliminar"
|
||||||
|
aria-label="Eliminar"
|
||||||
|
onClick={() => openDeleteModal(importer)}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/* Modal moved outside table for valid JSX and Users.jsx style will be applied below */}
|
||||||
|
{/* MODALS: Styled like Users.jsx, rendered outside the table for valid JSX */}
|
||||||
|
{modalOpen && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm overflow-y-auto h-full w-full z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="relative mx-auto w-full max-w-xl bg-white rounded-2xl shadow-2xl transform transition-all duration-300 animate-in slide-in-from-bottom-4">
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className={`bg-gradient-to-r ${modalMode === 'delete' ? 'from-red-700 to-red-900 border-red-500' : 'from-blue-700 to-blue-900 border-blue-500'} rounded-t-2xl p-4 text-white border-b-2`}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className={`${modalMode === 'delete' ? 'bg-red-500 border-red-400' : 'bg-blue-500 border-blue-400'} bg-opacity-30 rounded-xl p-2 border border-opacity-30`}>
|
||||||
|
{modalMode === 'delete' ? (
|
||||||
|
<svg className="w-5 h-5" 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>
|
||||||
|
) : (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold tracking-wide">
|
||||||
|
{modalMode === 'delete' ? 'Eliminar Importador' : modalMode === 'edit' ? 'Editar Importador' : 'Ver Importador'}
|
||||||
|
</h3>
|
||||||
|
<p className={`${modalMode === 'delete' ? 'text-red-200' : 'text-blue-200'} text-xs font-medium`}>
|
||||||
|
{modalMode === 'delete' ? 'Esta acción no se puede deshacer.' : 'Sistema de Gestión de Importadores'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={closeModal}
|
||||||
|
className={`${modalMode === 'delete' ? 'text-red-100 hover:text-white hover:bg-red-600' : 'text-blue-100 hover:text-white hover:bg-blue-600'} transition-colors p-2 hover:bg-opacity-50 rounded-lg border ${modalMode === 'delete' ? 'border-red-500' : 'border-blue-500'} border-opacity-30`}
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Content */}
|
||||||
|
<div className="p-6">
|
||||||
|
{modalMode === 'delete' ? (
|
||||||
|
<div className="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 Importador?</h3>
|
||||||
|
<div className="mt-2 px-7 py-3">
|
||||||
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
|
¿Estás seguro que deseas eliminar el importador <strong>{modalData.nombre}</strong> (RFC: <strong>{modalData.rfc}</strong>)?
|
||||||
|
</p>
|
||||||
|
<div className="bg-gray-50 rounded-md p-3 mb-4">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<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="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 className="ml-3 text-left">
|
||||||
|
<p className="text-sm font-medium text-gray-900">{modalData.nombre}</p>
|
||||||
|
<p className="text-xs text-gray-500">RFC: {modalData.rfc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{errorMsg && <div className="text-red-500 mb-2 w-full">{errorMsg}</div>}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center space-x-3 pt-4">
|
||||||
|
<button type="button" onClick={closeModal} disabled={modalLoading} className="px-6 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-all duration-200 disabled:opacity-50">Cancelar</button>
|
||||||
|
<button type="button" onClick={handleDeleteConfirm} disabled={modalLoading} className="px-6 py-2 border border-transparent rounded-md shadow-lg text-sm font-semibold text-white bg-gradient-to-r from-red-700 to-red-900 hover:from-red-800 hover:to-red-950 focus:ring-red-500 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 flex items-center justify-center space-x-2 disabled:opacity-50 disabled:cursor-not-allowed">{modalLoading ? (<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>) : null}{modalLoading ? 'Eliminando...' : 'Eliminar Importador'}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setModalLoading(true);
|
||||||
|
setErrorMsg('');
|
||||||
|
try {
|
||||||
|
let res;
|
||||||
|
if (modalMode === 'edit') {
|
||||||
|
res = await fetchWithAuth(`${API_URL}/customs/importadores/${modalData.rfc}/`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ rfc: modalData.rfc, nombre: modalData.nombre, organizacion: modalData.organizacion })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Error al actualizar importador');
|
||||||
|
} else if (modalMode === 'create') {
|
||||||
|
res = await fetchWithAuth(`${API_URL}/customs/importadores/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ rfc: modalData.rfc, nombre: modalData.nombre, organizacion: modalData.organizacion })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Error al crear importador');
|
||||||
|
}
|
||||||
|
// Refrescar lista
|
||||||
|
const res2 = await fetchWithAuth(`${API_URL}/customs/importadores/`);
|
||||||
|
if (!res2.ok) throw new Error('Error al obtener importadores');
|
||||||
|
const data = await res2.json();
|
||||||
|
setImporters(Array.isArray(data) ? data : []);
|
||||||
|
closeModal();
|
||||||
|
} catch {
|
||||||
|
setErrorMsg(modalMode === 'create' ? 'Error al crear importador' : 'Error al actualizar importador');
|
||||||
|
} finally {
|
||||||
|
setModalLoading(false);
|
||||||
|
}
|
||||||
|
}} className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-700 mb-1 text-left">RFC</label>
|
||||||
|
<input
|
||||||
|
name="rfc"
|
||||||
|
type="text"
|
||||||
|
value={modalData.rfc}
|
||||||
|
readOnly={modalMode !== 'create'}
|
||||||
|
className={`w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 ${modalMode === 'create' ? 'bg-white' : 'bg-gray-100'} text-slate-900 placeholder-slate-400 text-sm`}
|
||||||
|
onChange={modalMode === 'create' ? handleModalChange : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-700 mb-1 text-left">Nombre</label>
|
||||||
|
<input
|
||||||
|
name="nombre"
|
||||||
|
type="text"
|
||||||
|
value={modalData.nombre}
|
||||||
|
onChange={modalMode !== 'view' ? handleModalChange : undefined}
|
||||||
|
readOnly={modalMode === 'view'}
|
||||||
|
className={`w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 ${modalMode === 'view' ? 'bg-gray-100' : 'bg-white'} text-slate-900 placeholder-slate-400 text-sm`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-slate-700 mb-1 text-left">Organización</label>
|
||||||
|
<input
|
||||||
|
name="organizacion"
|
||||||
|
type="text"
|
||||||
|
value={modalData.organizacion}
|
||||||
|
onChange={modalMode !== 'view' ? handleModalChange : undefined}
|
||||||
|
readOnly={modalMode === 'view'}
|
||||||
|
className={`w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 ${modalMode === 'view' ? 'bg-gray-100' : 'bg-white'} text-slate-900 placeholder-slate-400 text-sm`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errorMsg && <div className="text-red-500 mt-2 text-sm text-center">{errorMsg}</div>}
|
||||||
|
<div className="flex flex-col sm:flex-row justify-end space-y-2 sm:space-y-0 sm:space-x-3 pt-4 border-t border-slate-200">
|
||||||
|
{modalMode !== 'view' && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={closeModal}
|
||||||
|
disabled={modalLoading}
|
||||||
|
className="w-full sm:w-auto px-6 py-2 border border-slate-300 rounded-md shadow-sm text-sm font-semibold text-slate-700 bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-all duration-200 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{(modalMode === 'edit' || modalMode === 'create') && (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={modalLoading}
|
||||||
|
className={`w-full sm:w-auto px-6 py-2 border border-transparent rounded-md shadow-lg text-sm font-semibold text-white bg-gradient-to-r from-blue-700 to-blue-900 hover:from-blue-800 hover:to-blue-950 focus:ring-blue-500 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 flex items-center justify-center space-x-2 disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||||
|
>
|
||||||
|
{modalLoading && (
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
|
)}
|
||||||
|
{modalLoading ? (modalMode === 'create' ? 'Creando...' : 'Actualizando...') : (modalMode === 'create' ? 'Crear' : 'Actualizar')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -321,19 +468,18 @@ export default function Importers() {
|
|||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
value={itemsPerPage}
|
value={itemsPerPage}
|
||||||
onChange={(e) => handleItemsPerPageChange(Number(e.target.value))}
|
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"
|
className="border border-gray-300 rounded-md px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
>
|
>
|
||||||
<option value={10}>10 por página</option>
|
<option value={10}>10 por página</option>
|
||||||
<option value={15}>15 por página</option>
|
<option value={15}>15 por página</option>
|
||||||
<option value={20}>20 por página</option>
|
<option value={20}>20 por página</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handlePageChange(currentPage - 1)}
|
onClick={e => handlePageChange(currentPage - 1, e)}
|
||||||
disabled={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"
|
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"
|
||||||
>
|
>
|
||||||
@@ -342,47 +488,35 @@ export default function Importers() {
|
|||||||
</svg>
|
</svg>
|
||||||
Anterior
|
Anterior
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="hidden sm:flex space-x-1">
|
<div className="hidden sm:flex space-x-1">
|
||||||
{[...Array(totalPages)].map((_, index) => {
|
{[...Array(totalPages)].map((_, index) => {
|
||||||
const page = index + 1;
|
const page = index + 1;
|
||||||
const isCurrentPage = page === currentPage;
|
const isCurrentPage = page === currentPage;
|
||||||
const isNearCurrentPage = Math.abs(page - currentPage) <= 2;
|
const isNearCurrentPage = Math.abs(page - currentPage) <= 2;
|
||||||
const isFirstOrLast = page === 1 || page === totalPages;
|
const isFirstOrLast = page === 1 || page === totalPages;
|
||||||
|
|
||||||
if (totalPages <= 7 || isNearCurrentPage || isFirstOrLast) {
|
if (totalPages <= 7 || isNearCurrentPage || isFirstOrLast) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={page}
|
key={page}
|
||||||
onClick={() => handlePageChange(page)}
|
onClick={e => handlePageChange(page, e)}
|
||||||
className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium rounded-md transition-colors ${
|
className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium rounded-md transition-colors ${isCurrentPage ? 'z-10 bg-blue-600 border-blue-600 text-white' : 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'}`}
|
||||||
isCurrentPage
|
|
||||||
? 'z-10 bg-navy-600 border-navy-600 text-white'
|
|
||||||
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{page}
|
{page}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
} else if (page === currentPage - 3 || page === currentPage + 3) {
|
} else if (page === currentPage - 3 || page === currentPage + 3) {
|
||||||
return (
|
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 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>
|
||||||
...
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sm:hidden flex items-center space-x-2">
|
<div className="sm:hidden flex items-center space-x-2">
|
||||||
<span className="text-sm text-gray-700">
|
<span className="text-sm text-gray-700">Página {currentPage} de {totalPages}</span>
|
||||||
Página {currentPage} de {totalPages}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handlePageChange(currentPage + 1)}
|
onClick={e => handlePageChange(currentPage + 1, e)}
|
||||||
disabled={currentPage === totalPages}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
@@ -397,22 +531,20 @@ export default function Importers() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Empty state */}
|
{/* Estado vacío */}
|
||||||
{currentImporters.length === 0 && !loading && (
|
{currentImporters.length === 0 && !loading && (
|
||||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-12">
|
<div className="bg-white rounded-xl shadow-lg border border-gray-200 p-6 sm:p-12">
|
||||||
<div className="text-center">
|
<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">
|
<div className="mx-auto h-20 w-20 sm:h-24 sm: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">
|
<svg className="h-10 w-10 sm:h-12 sm: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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">No se encontraron importadores</h3>
|
<h3 className="text-base sm:text-lg font-semibold text-gray-900 mb-2">No se encontraron importadores</h3>
|
||||||
<p className="text-gray-500 mb-6">
|
<p className="text-gray-500 mb-6">{searchTerm ? 'Intenta con otros términos de búsqueda.' : 'Comienza agregando un nuevo importador.'}</p>
|
||||||
{searchTerm ? 'Intenta con otros términos de búsqueda.' : 'Comienza agregando un nuevo importador.'}
|
|
||||||
</p>
|
|
||||||
{!searchTerm && (
|
{!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">
|
<button className="inline-flex items-center px-4 py-2 border border-transparent rounded-lg shadow-sm text-xs sm:text-sm font-medium 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">
|
||||||
<svg className="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="-ml-1 mr-2 h-4 w-4 sm:h-5 sm: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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
</svg>
|
</svg>
|
||||||
Agregar primer importador
|
Agregar primer importador
|
||||||
|
|||||||
@@ -1,8 +1,213 @@
|
|||||||
|
// Modal para relacionar importadores a una credencial VUCEM
|
||||||
|
function RelacionarImportadoresModal({ open, onClose, vucem }) {
|
||||||
|
const [importadoresDisponibles, setImportadoresDisponibles] = React.useState([]);
|
||||||
|
const [importadoresSeleccionados, setImportadoresSeleccionados] = React.useState([]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!open || !vucem) return;
|
||||||
|
// Obtener ambos listados en paralelo
|
||||||
|
Promise.all([
|
||||||
|
fetchWithAuth(import.meta.env.VITE_EFC_API_URL + '/customs/importadores/').then(res => res.json()),
|
||||||
|
fetchWithAuth(import.meta.env.VITE_EFC_API_URL + `/vucem/usuario-importador/?vucem=${vucem.id}`).then(res => res.json())
|
||||||
|
]).then(([importadoresAll, relaciones]) => {
|
||||||
|
// Agrupar relaciones por RFC, tomar la más reciente (última) para cada RFC
|
||||||
|
const relacionesPorRfc = {};
|
||||||
|
relaciones.results.forEach(rel => {
|
||||||
|
relacionesPorRfc[rel.rfc] = rel; // sobrescribe, así queda la última
|
||||||
|
});
|
||||||
|
const seleccionados = Object.values(relacionesPorRfc).map(rel => {
|
||||||
|
const base = importadoresAll.find(i => i.rfc === rel.rfc) || {};
|
||||||
|
return {
|
||||||
|
...rel,
|
||||||
|
nombre: base.nombre || rel.rfc,
|
||||||
|
organizacion: base.organizacion || rel.organizacion
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setImportadoresSeleccionados(seleccionados);
|
||||||
|
// Disponibles = todos menos los seleccionados (por RFC)
|
||||||
|
const seleccionadosRFC = new Set(seleccionados.map(i => i.rfc));
|
||||||
|
setImportadoresDisponibles(importadoresAll.filter(i => !seleccionadosRFC.has(i.rfc)));
|
||||||
|
});
|
||||||
|
}, [open, vucem]);
|
||||||
|
|
||||||
|
if (!open || !vucem) return null;
|
||||||
|
|
||||||
|
// Mover importador de disponibles a seleccionados
|
||||||
|
const seleccionarImportador = async (imp) => {
|
||||||
|
// POST para crear la relación
|
||||||
|
try {
|
||||||
|
// Usar siempre la organización del registro vucem
|
||||||
|
const body = {
|
||||||
|
vucem: vucem.id,
|
||||||
|
rfc: imp.rfc,
|
||||||
|
organizacion: vucem.organizacion
|
||||||
|
};
|
||||||
|
const res = await fetchWithAuth(
|
||||||
|
import.meta.env.VITE_EFC_API_URL + '/vucem/usuario-importador/',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!res.ok) throw new Error('Error al relacionar importador');
|
||||||
|
const data = await res.json();
|
||||||
|
// Guardar el id de la relación en el importador seleccionado
|
||||||
|
const impConRelacion = {
|
||||||
|
...imp,
|
||||||
|
id: data.id, // para que el botón de quitar funcione correctamente
|
||||||
|
usuario_importador_id: data.id
|
||||||
|
};
|
||||||
|
setImportadoresDisponibles(importadoresDisponibles.filter(i => i.rfc !== imp.rfc));
|
||||||
|
setImportadoresSeleccionados([...importadoresSeleccionados, impConRelacion]);
|
||||||
|
} catch (err) {
|
||||||
|
alert('No se pudo relacionar el importador.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Mover importador de seleccionados a disponibles
|
||||||
|
const quitarImportador = async (imp) => {
|
||||||
|
// DELETE para eliminar la relación usando el id de la relación
|
||||||
|
try {
|
||||||
|
// Buscar el id de la relación usuario-importador en el objeto importador
|
||||||
|
const relacionId = imp.usuario_importador_id || imp.id_relacion || imp.id;
|
||||||
|
if (!relacionId) {
|
||||||
|
alert('No se encontró el id de la relación usuario-importador.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `${import.meta.env.VITE_EFC_API_URL}/vucem/usuario-importador/${relacionId}/`;
|
||||||
|
const res = await fetchWithAuth(url, { method: 'DELETE' });
|
||||||
|
if (!res.ok) throw new Error('Error al eliminar relación');
|
||||||
|
setImportadoresSeleccionados(importadoresSeleccionados.filter(i => i.rfc !== imp.rfc));
|
||||||
|
setImportadoresDisponibles([...importadoresDisponibles, imp]);
|
||||||
|
} catch (err) {
|
||||||
|
alert('No se pudo eliminar la relación del importador.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-40">
|
||||||
|
<div className="relative mx-auto w-full max-w-3xl bg-white rounded-lg shadow-2xl transform transition-all duration-300 animate-in slide-in-from-bottom-4">
|
||||||
|
{/* Header formal en escala de azules */}
|
||||||
|
<div className="bg-gradient-to-r from-blue-700 to-blue-900 rounded-t-lg p-4 text-white border-b-2 border-blue-500">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="bg-blue-500 bg-opacity-30 rounded-lg p-2 border border-blue-400 border-opacity-30">
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold tracking-wide">Relacionar Importadores</h3>
|
||||||
|
<p className="text-blue-200 text-xs font-medium">Asocia importadores a la credencial seleccionada</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-blue-100 hover:text-white transition-colors p-2 hover:bg-blue-600 hover:bg-opacity-50 rounded-lg border border-blue-500 border-opacity-30"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-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>
|
||||||
|
</div>
|
||||||
|
{/* Contenido del modal */}
|
||||||
|
<div className="p-4 max-h-[85vh] overflow-y-auto">
|
||||||
|
<div className="flex flex-col md:flex-row md:justify-between md:items-center mb-4 gap-2 border-b pb-2 border-blue-100">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Usuario:</span>
|
||||||
|
<span className="inline-block bg-blue-100 text-blue-700 rounded px-2 py-0.5 text-xs font-bold shadow-sm">{vucem.usuario}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Patente:</span>
|
||||||
|
<span className="inline-block bg-blue-100 text-blue-700 rounded px-2 py-0.5 text-xs font-bold shadow-sm">{vucem.patente}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full">
|
||||||
|
{/* Importadores disponibles */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-blue-700 mb-2">Disponibles</h3>
|
||||||
|
<ul className="border border-blue-100 rounded-lg divide-y divide-blue-50 bg-gray-50 shadow-sm">
|
||||||
|
{importadoresDisponibles.length === 0 && (
|
||||||
|
<li className="text-gray-400 text-center py-3 text-sm">Sin importadores</li>
|
||||||
|
)}
|
||||||
|
{importadoresDisponibles.map(imp => (
|
||||||
|
<li
|
||||||
|
key={imp.id}
|
||||||
|
className="px-3 py-2 cursor-pointer hover:bg-blue-100/70 transition rounded flex items-center gap-2 group text-sm"
|
||||||
|
onClick={() => seleccionarImportador(imp)}
|
||||||
|
>
|
||||||
|
<span className="flex-1 font-medium text-gray-700 truncate">
|
||||||
|
<span className="inline-block bg-white border border-blue-200 text-blue-700 rounded px-2 py-0.5 text-xs font-semibold mr-2 align-middle shadow-sm">
|
||||||
|
{imp.rfc}
|
||||||
|
</span>
|
||||||
|
{imp.nombre}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="ml-2 px-3 py-1 bg-blue-600 text-white rounded shadow hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 text-xs font-semibold"
|
||||||
|
title="Agregar"
|
||||||
|
>
|
||||||
|
Agregar
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/* Importadores seleccionados */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-blue-700 mb-2">Seleccionados</h3>
|
||||||
|
<ul className="border border-blue-200 rounded-lg divide-y divide-blue-100 bg-blue-50 shadow-sm">
|
||||||
|
{importadoresSeleccionados.length === 0 && (
|
||||||
|
<li className="text-gray-400 text-center py-3 text-sm">Sin seleccionados</li>
|
||||||
|
)}
|
||||||
|
{importadoresSeleccionados.map(imp => (
|
||||||
|
<li
|
||||||
|
key={imp.id}
|
||||||
|
className="px-3 py-2 cursor-pointer hover:bg-blue-200/80 transition rounded flex items-center gap-2 group text-sm"
|
||||||
|
onClick={() => quitarImportador(imp)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="mr-2 px-3 py-1 bg-gray-300 text-gray-800 rounded shadow hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-400 text-xs font-semibold"
|
||||||
|
title="Quitar"
|
||||||
|
>
|
||||||
|
Quitar
|
||||||
|
</button>
|
||||||
|
<span className="flex-1 font-medium text-gray-700 truncate">
|
||||||
|
<span className="inline-block bg-white border border-blue-300 text-blue-700 rounded px-2 py-0.5 text-xs font-semibold mr-2 align-middle shadow-sm">
|
||||||
|
{imp.rfc}
|
||||||
|
</span>
|
||||||
|
{imp.nombre}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end mt-4 w-full">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-5 py-1.5 bg-blue-600 text-white rounded-lg font-semibold shadow hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm"
|
||||||
|
>
|
||||||
|
Cerrar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style>{`
|
||||||
|
@keyframes fade-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
|
||||||
|
.animate-fade-in { animation: fade-in 0.3s ease; }
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { fetchWithAuth, postWithAuth, putWithAuth, deleteWithAuth, putFormDataWithAuth, postFormDataWithAuth, patchWithAuth } from '../fetchWithAuth';
|
import { fetchWithAuth, postWithAuth, putWithAuth, deleteWithAuth, putFormDataWithAuth, postFormDataWithAuth, patchWithAuth } from '../fetchWithAuth';
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||||
export default function Vucem() {
|
export default function Vucem() {
|
||||||
|
// Estado para modal de relacionar importadores
|
||||||
|
const [showRelacionarModal, setShowRelacionarModal] = useState(false);
|
||||||
|
const [selectedVucem, setSelectedVucem] = useState(null);
|
||||||
const [vucemList, setVucemList] = useState([]);
|
const [vucemList, setVucemList] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@@ -292,6 +497,14 @@ export default function Vucem() {
|
|||||||
// Table y header estilo Users.jsx
|
// Table y header estilo Users.jsx
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-7xl mx-auto">
|
<div className="p-6 max-w-7xl mx-auto">
|
||||||
|
{/* Modal Relacionar Importadores */}
|
||||||
|
{showRelacionarModal && selectedVucem && (
|
||||||
|
<RelacionarImportadoresModal
|
||||||
|
open={showRelacionarModal}
|
||||||
|
onClose={() => setShowRelacionarModal(false)}
|
||||||
|
vucem={selectedVucem}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{/* Header modernizado con gradientes azules */}
|
{/* Header modernizado con gradientes azules */}
|
||||||
<div className="mb-8 relative overflow-hidden rounded-2xl shadow-xl bg-gradient-to-br from-blue-600 via-blue-700 to-blue-800 p-6 sm:p-8">
|
<div className="mb-8 relative overflow-hidden rounded-2xl shadow-xl bg-gradient-to-br from-blue-600 via-blue-700 to-blue-800 p-6 sm:p-8">
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/20 via-transparent to-blue-900/30"></div>
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/20 via-transparent to-blue-900/30"></div>
|
||||||
@@ -638,6 +851,18 @@ export default function Vucem() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||||
<div className="flex justify-center space-x-2">
|
<div className="flex justify-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedVucem(vucem);
|
||||||
|
setShowRelacionarModal(true);
|
||||||
|
}}
|
||||||
|
className="inline-flex items-center p-1 border border-purple-300 shadow-sm rounded text-purple-600 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-200 transition-all duration-200"
|
||||||
|
title="Relacionar importadores"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setEditVucem(vucem); setShowEditModal(true); }}
|
onClick={() => { setEditVucem(vucem); setShowEditModal(true); }}
|
||||||
className="inline-flex items-center p-2 border border-blue-300 shadow-sm font-medium rounded-lg text-blue-700 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105"
|
className="inline-flex items-center p-2 border border-blue-300 shadow-sm font-medium rounded-lg text-blue-700 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105"
|
||||||
@@ -736,6 +961,16 @@ export default function Vucem() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleAbrirRelacion(vucem)}
|
||||||
|
className="inline-flex items-center justify-center p-2 border border-purple-300 shadow-sm font-medium rounded-lg text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200"
|
||||||
|
title="Relacionar importadores"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
<span className="hidden sm:inline text-xs">Relacionar</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setEditVucem(vucem); setShowEditModal(true); }}
|
onClick={() => { setEditVucem(vucem); setShowEditModal(true); }}
|
||||||
className="inline-flex items-center justify-center p-2 border border-blue-300 shadow-sm font-medium rounded-lg text-blue-700 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
|
className="inline-flex items-center justify-center p-2 border border-blue-300 shadow-sm font-medium rounded-lg text-blue-700 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
|
||||||
@@ -745,6 +980,7 @@ export default function Vucem() {
|
|||||||
<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" />
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleVucemStatus(vucem.id, vucem.is_active)}
|
onClick={() => toggleVucemStatus(vucem.id, vucem.is_active)}
|
||||||
className={`inline-flex items-center justify-center p-2 border shadow-sm font-medium rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 ${vucem.is_active
|
className={`inline-flex items-center justify-center p-2 border shadow-sm font-medium rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 ${vucem.is_active
|
||||||
|
|||||||
Reference in New Issue
Block a user