Se agregaron datos del ticker 2025-08-046

This commit is contained in:
2025-08-20 09:15:59 -06:00
parent 2bc70fc3c2
commit 3e498c57ad
7 changed files with 889 additions and 395 deletions

View File

@@ -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(

View 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);
}

View File

@@ -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
? [] ? []

View File

@@ -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 && (

View File

@@ -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>

View File

@@ -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

View File

@@ -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