diff --git a/src/api/expedientes.ts b/src/api/expedientes.ts
index 7d4245f..d062b00 100644
--- a/src/api/expedientes.ts
+++ b/src/api/expedientes.ts
@@ -24,16 +24,35 @@ const API_URL = import.meta.env.VITE_EFC_API_URL;
// Obtiene la lista de documentos (pedimentos)
export interface PedimentosFilters {
search?: string;
+ id?: string;
+ documentos_count?: number;
+ documentos_peso_total?: number;
pedimento?: string;
- existe_expediente?: string | boolean;
- alerta?: string | boolean;
- contribuyente?: string;
- curp_apoderado?: string;
- fecha_pago?: string;
+ pedimento_app?: string;
patente?: string;
aduana?: string;
- tipo_operacion?: string;
+ regimen?: 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(
diff --git a/src/assets/animate-fade-in-fast.css b/src/assets/animate-fade-in-fast.css
new file mode 100644
index 0000000..6fc16f0
--- /dev/null
+++ b/src/assets/animate-fade-in-fast.css
@@ -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);
+}
diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx
index c6a1998..968427a 100644
--- a/src/components/Sidebar.jsx
+++ b/src/components/Sidebar.jsx
@@ -183,6 +183,16 @@ export default function Sidebar({ isMobileOpen, onMobileClose }) {
{
title: 'Acceso a Usuarios',
items: [
+ // Botón Importadores como primer elemento
+ {
+ name: 'Importadores',
+ path: '/importers',
+ icon: (
+
+
+
+ )
+ },
...(
isImportador
? []
diff --git a/src/pages/Datastage.jsx b/src/pages/Datastage.jsx
index 4642ca7..a028e9f 100644
--- a/src/pages/Datastage.jsx
+++ b/src/pages/Datastage.jsx
@@ -60,8 +60,10 @@ function RegistrosCargadosModal({ open, onClose, registros }) {
}
// 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 {
+ setEnProcesoId(item.id);
const url = `${import.meta.env.VITE_EFC_API_URL}/datastage/datastages/${item.id}/procesar/`;
const body = {
organizacion: item.organizacion,
@@ -88,6 +90,8 @@ async function procesarDatastage(item, setDatastages, setSuccess, setError, setR
}
} catch (e) {
setError('No se pudo procesar el datastage');
+ } finally {
+ setEnProcesoId(null);
}
}
// Descarga autenticada de archivos datastage
@@ -128,6 +132,8 @@ export default function Datastage() {
// Animación header
const [showAnimation, setShowAnimation] = useState(false);
const [hasAnimated, setHasAnimated] = useState(false);
+ // Estado para mostrar mensaje "En proceso" por cada datastage
+ const [enProcesoId, setEnProcesoId] = useState(null);
useLayoutEffect(() => { setShowAnimation(true); }, []);
useEffect(() => { if (showAnimation && !hasAnimated) setTimeout(() => setHasAnimated(true), 800); }, [showAnimation, hasAnimated]);
@@ -180,46 +186,6 @@ export default function Datastage() {
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
const openEditModal = (item) => {
setForm({ archivo: null, contribuyente: item.contribuyente });
@@ -403,14 +369,18 @@ export default function Datastage() {
procesarDatastage(item, setDatastages, setSuccess, setError, setRegistrosCargados, setShowRegistrosModal)}
- 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'}`}
- title={item.procesado ? 'Ya procesado' : 'Procesar'}
- disabled={item.procesado}
+ onClick={() => procesarDatastage(item, setDatastages, setSuccess, setError, setRegistrosCargados, setShowRegistrosModal, setEnProcesoId)}
+ 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' : enProcesoId === item.id ? 'En proceso' : 'Procesar'}
+ disabled={item.procesado || enProcesoId === item.id}
>
-
-
-
+ {item.procesado ? (
+ Procesado
+ ) : enProcesoId === item.id ? (
+ En proceso...
+ ) : (
+ Procesar
+ )}
@@ -480,14 +450,18 @@ export default function Datastage() {
procesarDatastage(item, setDatastages, setSuccess, setError, setRegistrosCargados, setShowRegistrosModal)}
- 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'}`}
- title={item.procesado ? 'Ya procesado' : 'Procesar'}
- disabled={item.procesado}
+ onClick={() => procesarDatastage(item, setDatastages, setSuccess, setError, setRegistrosCargados, setShowRegistrosModal, setEnProcesoId)}
+ 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' : enProcesoId === item.id ? 'En proceso' : 'Procesar'}
+ disabled={item.procesado || enProcesoId === item.id}
>
-
-
-
+ {item.procesado ? (
+ Procesado
+ ) : enProcesoId === item.id ? (
+ En proceso...
+ ) : (
+ Procesar
+ )}
@@ -497,44 +471,141 @@ export default function Datastage() {
{/* Modales */}
- {/* Modal de creación */}
- {showCreateModal && (
-
- )}
- {/* Modal de edición */}
- {showEditModal && (
-
- )}
+ {/* Modal de creación - estilo Users/Importers */}
+ {showCreateModal && (
+
+ )}
- {/* Modal de confirmación para eliminar */}
- setShowDeleteModal(false)} onConfirm={handleDelete} message="¿Seguro que deseas eliminar este datastage?" confirmText="Eliminar" cancelText="Cancelar" />
+
+ {/* Modal de edición - estilo Users/Importers */}
+ {showEditModal && (
+
+ )}
+
+
+ {/* Modal de eliminación - estilo Users/Importers */}
+ {showDeleteModal && (
+
+
+ {/* Header */}
+
+
+
+
+
+
Eliminar Datastage
+
Esta acción no se puede deshacer.
+
+
+
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">
+
+
+
+
+
+
+ {/* Content */}
+
+
+
¿Eliminar este datastage?
+
¿Seguro que deseas eliminar este datastage? Esta acción no se puede deshacer.
+
+ 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
+ Eliminar
+
+
+
+
+ )}
{/* Modal de detalle */}
{showDetailModal && selected && (
diff --git a/src/pages/Expedientes.jsx b/src/pages/Expedientes.jsx
index 3305799..dac402e 100644
--- a/src/pages/Expedientes.jsx
+++ b/src/pages/Expedientes.jsx
@@ -419,8 +419,11 @@ export default function Documents() {
Contribuyente
CURP Apoderado
Partidas
- Saldo disponible
- Importe pedimento
+ Fecha de Carga
+ Tipo Operacion
+ Clave
+ No. Archivos
+ Peso Total
Expediente
@@ -462,8 +465,11 @@ export default function Documents() {
{ped.contribuyente}
{ped.curp_apoderado}
{ped.numero_partidas}
- ${ped.saldo_disponible}
- ${ped.importe_pedimento_app}
+ {ped.created_at ? ped.created_at.slice(0, 10) : ''}
+ {ped.tipo_operacion}
+ {ped.clave_pedimento}
+ {ped.documentos_count}
+ {ped.documentos_peso_total}
)}
+
+ Tipo Operacion
+ {ped.tipo_operacion}
+
Partidas
- ${ped.numero_partidas}
+ {ped.numero_partidas}
- Saldo disponible:
- ${ped.saldo_disponible}
+ Fecha de Carga
+ {ped.created_at ? ped.created_at.slice(0, 10) : ''}
- Importe pedimento:
- ${ped.importe_pedimento_app}
+ Clave
+ {ped.clave_pedimento}
+
+
+ No. Archivos
+
+
+
+ Peso Total
+ Peso total
diff --git a/src/pages/Importers.jsx b/src/pages/Importers.jsx
index 5211412..9768dcd 100644
--- a/src/pages/Importers.jsx
+++ b/src/pages/Importers.jsx
@@ -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() {
+ const focusKeeperRef = useRef(null);
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 [searchTerm, setSearchTerm] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
+ const [showAnimation, setShowAnimation] = useState(false);
+ const [hasAnimated, setHasAnimated] = useState(false);
- // Datos dummy para mostrar
- const dummyImporters = [
- {
- id: 1,
- name: 'Importadora ABC S.A.',
- rfc: 'ABC123456789',
- email: 'contacto@abc.com',
- status: 'Activo',
- lastActivity: '2024-01-15',
- documentsCount: 45
- },
- {
- id: 2,
- name: 'Comercial XYZ Ltda.',
- rfc: 'XYZ987654321',
- email: 'info@xyz.com',
- status: 'Activo',
- lastActivity: '2024-01-14',
- documentsCount: 23
- },
- {
- id: 3,
- name: 'Global Trade Corp.',
- rfc: 'GTC555666777',
- email: 'admin@globaltrade.com',
- status: 'Inactivo',
- lastActivity: '2024-01-10',
- documentsCount: 12
+ useLayoutEffect(() => { setShowAnimation(true); }, []);
+ useEffect(() => {
+ if (showAnimation && !hasAnimated) {
+ const timeout = setTimeout(() => {
+ setHasAnimated(true);
+ setShowAnimation(false);
+ }, 700);
+ return () => clearTimeout(timeout);
}
- ];
+ }, [showAnimation, hasAnimated]);
useEffect(() => {
- // Simular carga de datos
- const timer = setTimeout(() => {
- setImporters(dummyImporters);
- setLoading(false);
- }, 1000);
-
- return () => clearTimeout(timer);
+ setLoading(true);
+ fetchWithAuth(`${API_URL}/customs/importadores/`)
+ .then(async res => {
+ if (!res.ok) throw new Error('Error al obtener importadores');
+ const data = await res.json();
+ setImporters(Array.isArray(data) ? data : []);
+ })
+ .catch(() => setImporters([]))
+ .finally(() => setLoading(false));
}, []);
const filteredImporters = importers.filter(importer =>
- importer.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- importer.rfc.toLowerCase().includes(searchTerm.toLowerCase()) ||
- importer.email.toLowerCase().includes(searchTerm.toLowerCase())
+ (importer.nombre || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
+ (importer.rfc || '').toLowerCase().includes(searchTerm.toLowerCase())
);
- // Cálculos de paginación
+ // Paginación
const totalImporters = filteredImporters.length;
- const totalPages = Math.ceil(totalImporters / itemsPerPage);
+ const totalPages = Math.ceil(totalImporters / itemsPerPage) || 1;
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentImporters = filteredImporters.slice(startIndex, endIndex);
- // Reset página cuando cambia el filtro
- useEffect(() => {
- setCurrentPage(1);
- }, [searchTerm]);
+ 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);
+ if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement) {
+ document.activeElement.blur();
+ }
};
-
- const handleItemsPerPageChange = (newItemsPerPage) => {
- setItemsPerPage(newItemsPerPage);
- setCurrentPage(1); // Reset a la primera página
- };
-
- const getStatusBadge = (status) => {
- return status === 'Activo'
- ? 'bg-success-100 text-success-800 border border-success-200'
- : 'bg-danger-100 text-danger-800 border border-danger-200';
- };
-
- if (loading) {
- return (
-
-
-
-
-
-
-
Cargando información de importadores...
-
-
- );
- }
+ useLayoutEffect(() => { if (focusKeeperRef.current) focusKeeperRef.current.focus(); }, [currentPage]);
+ const handleItemsPerPageChange = (newItemsPerPage) => { setItemsPerPage(newItemsPerPage); setCurrentPage(1); };
+ // No hay status real en el endpoint, así que lo dejamos azul
+ const getStatusBadge = () => 'bg-blue-100 text-blue-800 border border-blue-200';
return (
-
+
+
- {/* Header */}
-
-
-
- Importadores
+ {/* Header animado y decorativo */}
+
+
+
+
+ Importadores
+ {totalImporters > 0 && (
+
+ {totalImporters} registros
+
+ )}
-
Gestiona y supervisa las empresas importadoras registradas en el sistema.
+
Gestiona y supervisa las empresas importadoras registradas en el sistema.
+
+ {/* Efectos decorativos de fondo modernos */}
+
+
+ {/* Partículas flotantes */}
+
+
- {/* Stats Cards */}
-
-
-
-
-
-
-
- Total Importadores
- {importers.length}
-
-
+ {/* Filtros y acciones */}
+
+
+
+
+
+
+
+
+ Filtros de búsqueda
+
+
{ 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"
+ >
+
+
+
+ Crear importador
+
-
-
-
-
-
-
-
-
-
- Activos
-
- {importers.filter(i => i.status === 'Activo').length}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Inactivos
-
- {importers.filter(i => i.status === 'Inactivo').length}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Total Documentos
-
- {importers.reduce((sum, i) => sum + i.documentsCount, 0)}
-
-
-
-
-
-
-
-
- {/* Search and Actions */}
-
-
-
-
-
-
+
-
-
-
-
-
- Nuevo Importador
-
-
- {/* Table */}
-
-
+ {/* Tabla de importadores */}
+
+
-
- Importador
-
-
- RFC
-
-
- Estado
-
-
- Documentos
-
-
- Última Actividad
-
-
- Acciones
-
+ RFC
+ Nombre
+ Organización
+ Creado
+ Actualizado
+ Acciones
{currentImporters.map((importer, index) => (
-
-
-
-
-
-
-
-
+
+ {importer.rfc}
+ {importer.nombre || Sin nombre }
+ {importer.organizacion}
+ {importer.created_at ? new Date(importer.created_at).toLocaleString() : ''}
+ {importer.updated_at ? new Date(importer.updated_at).toLocaleString() : ''}
+
+
+
openViewModal(importer)}
+ >
+
+
+
+
+
+
openEditModal(importer)}
+ >
+
+
+
+
+
openDeleteModal(importer)}
+ >
+
+
+
+
+ {/* 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 && (
+
+
+ {/* Modal Header */}
+
+
+
+
+ {modalMode === 'delete' ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {modalMode === 'delete' ? 'Eliminar Importador' : modalMode === 'edit' ? 'Editar Importador' : 'Ver Importador'}
+
+
+ {modalMode === 'delete' ? 'Esta acción no se puede deshacer.' : 'Sistema de Gestión de Importadores'}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Modal Content */}
+
+ {modalMode === 'delete' ? (
+
+
+
¿Eliminar Importador?
+
+
+ ¿Estás seguro que deseas eliminar el importador {modalData.nombre} (RFC: {modalData.rfc} )?
+
+
+
+
+
+
{modalData.nombre}
+
RFC: {modalData.rfc}
+
+
+
+ {errorMsg &&
{errorMsg}
}
+
+
+
Cancelar
+
{modalLoading ? (
) : null}{modalLoading ? 'Eliminando...' : 'Eliminar Importador'}
+
+
+ ) : (
+
+ )}
+
+
-
-
-
{importer.name}
-
{importer.email}
-
-
-
-
- {importer.rfc}
-
-
-
- {importer.status}
-
-
-
-
- {importer.documentsCount}
-
- docs
-
-
-
-
- {new Date(importer.lastActivity).toLocaleDateString()}
-
-
-
-
- Ver
-
-
- Editar
-
-
- Eliminar
-
+ )}
@@ -321,19 +468,18 @@ export default function Importers() {
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"
+ 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-blue-500 focus:border-blue-500"
>
10 por página
15 por página
20 por página
-
{totalPages > 1 && (
handlePageChange(currentPage - 1)}
+ onClick={e => handlePageChange(currentPage - 1, e)}
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"
>
@@ -342,47 +488,35 @@ export default function Importers() {
Anterior
-
{[...Array(totalPages)].map((_, index) => {
const page = index + 1;
const isCurrentPage = page === currentPage;
const isNearCurrentPage = Math.abs(page - currentPage) <= 2;
const isFirstOrLast = page === 1 || page === totalPages;
-
if (totalPages <= 7 || isNearCurrentPage || isFirstOrLast) {
return (
handlePageChange(page)}
- className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium rounded-md transition-colors ${
- isCurrentPage
- ? 'z-10 bg-navy-600 border-navy-600 text-white'
- : 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
- }`}
+ onClick={e => handlePageChange(page, e)}
+ 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'}`}
>
{page}
);
} else if (page === currentPage - 3 || page === currentPage + 3) {
return (
-
- ...
-
+ ...
);
}
return null;
})}
-
-
- Página {currentPage} de {totalPages}
-
+ Página {currentPage} de {totalPages}
-
handlePageChange(currentPage + 1)}
+ onClick={e => handlePageChange(currentPage + 1, e)}
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"
>
@@ -397,22 +531,20 @@ export default function Importers() {
)}
- {/* Empty state */}
+ {/* Estado vacío */}
{currentImporters.length === 0 && !loading && (
-
+
-
-
+
- No se encontraron importadores
-
- {searchTerm ? 'Intenta con otros términos de búsqueda.' : 'Comienza agregando un nuevo importador.'}
-
+ No se encontraron importadores
+ {searchTerm ? 'Intenta con otros términos de búsqueda.' : 'Comienza agregando un nuevo importador.'}
{!searchTerm && (
-
-
+
+
Agregar primer importador
diff --git a/src/pages/Vucem.jsx b/src/pages/Vucem.jsx
index def3220..ecf0642 100644
--- a/src/pages/Vucem.jsx
+++ b/src/pages/Vucem.jsx
@@ -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 (
+
+
+ {/* Header formal en escala de azules */}
+
+
+
+
+
+
Relacionar Importadores
+
Asocia importadores a la credencial seleccionada
+
+
+
+
+
+
+
+
+
+ {/* Contenido del modal */}
+
+
+
+ Usuario:
+ {vucem.usuario}
+
+
+ Patente:
+ {vucem.patente}
+
+
+
+ {/* Importadores disponibles */}
+
+
Disponibles
+
+ {importadoresDisponibles.length === 0 && (
+ Sin importadores
+ )}
+ {importadoresDisponibles.map(imp => (
+ seleccionarImportador(imp)}
+ >
+
+
+ {imp.rfc}
+
+ {imp.nombre}
+
+
+ Agregar
+
+
+ ))}
+
+
+ {/* Importadores seleccionados */}
+
+
Seleccionados
+
+ {importadoresSeleccionados.length === 0 && (
+ Sin seleccionados
+ )}
+ {importadoresSeleccionados.map(imp => (
+ quitarImportador(imp)}
+ >
+
+ Quitar
+
+
+
+ {imp.rfc}
+
+ {imp.nombre}
+
+
+ ))}
+
+
+
+
+
+ Cerrar
+
+
+
+
+
+
+ );
+}
import React, { useEffect, useState } from 'react';
import { fetchWithAuth, postWithAuth, putWithAuth, deleteWithAuth, putFormDataWithAuth, postFormDataWithAuth, patchWithAuth } from '../fetchWithAuth';
const API_URL = import.meta.env.VITE_EFC_API_URL;
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 [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -292,6 +497,14 @@ export default function Vucem() {
// Table y header estilo Users.jsx
return (
+ {/* Modal Relacionar Importadores */}
+ {showRelacionarModal && selectedVucem && (
+
setShowRelacionarModal(false)}
+ vucem={selectedVucem}
+ />
+ )}
{/* Header modernizado con gradientes azules */}
@@ -638,6 +851,18 @@ export default function Vucem() {
+
{
+ 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"
+ >
+
+
+
+
{ 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"
@@ -736,6 +961,16 @@ export default function Vucem() {
+
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"
+ >
+
+
+
+ Relacionar
+
{ 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"
@@ -745,6 +980,7 @@ export default function Vucem() {
+
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