// 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}
  • ))}
{/* Importadores seleccionados */}

Seleccionados

    {importadoresSeleccionados.length === 0 && (
  • Sin seleccionados
  • )} {importadoresSeleccionados.map(imp => (
  • quitarImportador(imp)} > {imp.rfc} {imp.nombre}
  • ))}
); } import React, { useEffect, useState } from 'react'; import { fetchWithAuth, postWithAuth, putWithAuth, deleteWithAuth, putFormDataWithAuth, postFormDataWithAuth, patchWithAuth } from '../fetchWithAuth'; import { useUser } from '../context/UserContext'; const API_URL = import.meta.env.VITE_EFC_API_URL; export default function Vucem() { // Estado para modal de relacionar importadores const isDebugMode = import.meta.env.VITE_DEBUG_MODE === 'true'; const [showRelacionarModal, setShowRelacionarModal] = useState(false); const [selectedVucem, setSelectedVucem] = useState(null); const [vucemList, setVucemList] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showCreateModal, setShowCreateModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [editVucem, setEditVucem] = useState(null); const [showDeleteModal, setShowDeleteModal] = useState(false); const [deleteVucem, setDeleteVucem] = useState(null); const [organizaciones, setOrganizaciones] = useState([]); const { user: currentUser } = useUser(); // Estado para formulario de creación/edición const initialForm = { usuario: '', organizacion: '', password: '', patente: '', efirma: '', key: null, cer: null, is_importador: false, acusecove: false, acuseedocument: false, is_active: true, }; const [form, setForm] = useState(initialForm); // Estados para controlar visibilidad de contraseñas y copiar const [showPassword, setShowPassword] = useState({}); const [showEfirma, setShowEfirma] = useState({}); const [copySuccess, setCopySuccess] = useState(''); // Estados específicos para modal de edición (siempre ocultos por defecto) const [showEditPassword, setShowEditPassword] = useState(false); const [showEditEfirma, setShowEditEfirma] = useState(false); // Estados específicos para modal de creación const [showCreatePassword, setShowCreatePassword] = useState(false); const [showCreateEfirma, setShowCreateEfirma] = useState(false); // Funciones para alternar visibilidad const togglePasswordVisibility = (id) => { setShowPassword(prev => ({ ...prev, [id]: !prev[id] })); }; const toggleEfirmaVisibility = (id) => { setShowEfirma(prev => ({ ...prev, [id]: !prev[id] })); }; useEffect(() => { const fetchOrganizaciones = async () => { try { const url = `${import.meta.env.VITE_EFC_API_URL}/organization/organizaciones/`; const res = await fetchWithAuth(url); // ← USA fetchWithAuth if (!res.ok) throw new Error('Error al obtener las organizaciones'); const data = await res.json(); setOrganizaciones(data); } catch (err) { console.error('Error fetching organizaciones:', err); setOrganizaciones([]); // ← Asegurar que siempre sea un array } }; fetchOrganizaciones(); }, []); // Funciones para alternar visibilidad en modal de edición const toggleEditPasswordVisibility = () => { setShowEditPassword(!showEditPassword); }; const toggleEditEfirmaVisibility = () => { setShowEditEfirma(!showEditEfirma); }; // Función helper para truncar texto largo en la tabla const truncateForTable = (text, maxLength = 20) => { if (!text || text.length <= maxLength) return text; const start = text.substring(0, 6); const end = text.substring(text.length - 6); return `${start}...${end}`; }; // Función para copiar al portapapeles const copyToClipboard = async (text, fieldName) => { try { // Verificar si el texto existe if (!text || text.trim() === '') { setCopySuccess(`${fieldName} está vacío`); setTimeout(() => setCopySuccess(''), 2000); return; } // Intentar usar la Clipboard API moderna if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); setCopySuccess(`${fieldName} copiado`); setTimeout(() => setCopySuccess(''), 2000); } else { // Fallback para navegadores más antiguos o contextos no seguros const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.opacity = '0'; textArea.style.pointerEvents = 'none'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { const successful = document.execCommand('copy'); if (successful) { setCopySuccess(`${fieldName} copiado`); } else { setCopySuccess(`Error al copiar ${fieldName}`); } } catch (fallbackErr) { console.error('Error en fallback:', fallbackErr); setCopySuccess(`Error al copiar ${fieldName}`); } finally { document.body.removeChild(textArea); setTimeout(() => setCopySuccess(''), 2000); } } } catch (err) { console.error('Error al copiar:', err); setCopySuccess(`Error al copiar ${fieldName}`); setTimeout(() => setCopySuccess(''), 2000); } }; // Handlers básicos para inputs const handleInputChange = e => { const { name, value, type, checked, files } = e.target; if (type === 'checkbox') { setForm(f => ({ ...f, [name]: checked })); } else if (type === 'file') { setForm(f => ({ ...f, [name]: files[0] })); } else { setForm(f => ({ ...f, [name]: value })); } }; const closeModals = () => { setShowCreateModal(false); setShowEditModal(false); setShowDeleteModal(false); setForm(initialForm); setEditVucem(null); setDeleteVucem(null); // Resetear estados de visibilidad del modal de edición setShowEditPassword(false); setShowEditEfirma(false); // Resetear estados de visibilidad del modal de creación setShowCreatePassword(false); setShowCreateEfirma(false); }; // Fetch list const fetchVucem = async () => { setLoading(true); try { const res = await fetchWithAuth(`${API_URL}/vucem/vucem/`); if (!res.ok) throw new Error('Error al cargar Ventanilla Unica'); const data = await res.json(); // console.log('data > ', data); setVucemList(data); setError(null); } catch (err) { setError('Error al cargar Ventanilla Unica'); } setLoading(false); }; // Funciones de descarga const downloadCertificate = async (id, usuario) => { try { const res = await fetchWithAuth(`${API_URL}/vucem/vucem/${id}/download_cer/`); if (!res.ok) { if (res.status === 404) { setCopySuccess('Certificado no encontrado'); setTimeout(() => setCopySuccess(''), 2000); return; } throw new Error('Error al descargar certificado'); } const blob = await res.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = `${usuario}_certificado.cer`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); setCopySuccess('Certificado descargado'); setTimeout(() => setCopySuccess(''), 2000); } catch (err) { console.error('Error al descargar certificado:', err); setCopySuccess('Error al descargar certificado'); setTimeout(() => setCopySuccess(''), 2000); } }; const downloadKey = async (id, usuario) => { try { const res = await fetchWithAuth(`${API_URL}/vucem/vucem/${id}/download_key/`); if (!res.ok) { if (res.status === 404) { setCopySuccess('Clave no encontrada'); setTimeout(() => setCopySuccess(''), 2000); return; } throw new Error('Error al descargar clave'); } const blob = await res.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = `${usuario}_clave.key`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); setCopySuccess('Clave descargada'); setTimeout(() => setCopySuccess(''), 2000); } catch (err) { console.error('Error al descargar clave:', err); setCopySuccess('Error al descargar clave'); setTimeout(() => setCopySuccess(''), 2000); } }; // Función para activar/desactivar credencial const toggleVucemStatus = async (id, currentStatus) => { try { const response = await patchWithAuth(`${API_URL}/vucem/vucem/${id}/`, { is_active: !currentStatus }); if (!response.ok) { throw new Error('Error al cambiar el estado'); } // Recargar la lista para reflejar los cambios await fetchVucem(); } catch (err) { alert('Error al cambiar el estado de la credencial'); } }; useEffect(() => { fetchVucem(); }, []); // Filtros visuales const [filterUsuario, setFilterUsuario] = useState(''); const [filterPatente, setFilterPatente] = useState(''); const filteredList = vucemList.filter(v => v.usuario.toLowerCase().includes(filterUsuario.toLowerCase()) && v.patente.toLowerCase().includes(filterPatente.toLowerCase()) ); // Paginación estilo Users.jsx const [page, setPage] = useState(1); const pageSize = 10; const totalPages = Math.max(1, Math.ceil(filteredList.length / pageSize)); const paginatedList = filteredList.slice((page - 1) * pageSize, page * pageSize); // Reset page si cambia el filtro useEffect(() => { setPage(1); }, [filterUsuario, filterPatente]); // Cuando se selecciona un registro para editar, poblar el formulario con sus datos useEffect(() => { if (editVucem) { setForm({ usuario: editVucem.usuario || '', password: editVucem.password || '', // Mostrar el password actual para edición patente: editVucem.patente || '', efirma: editVucem.efirma || '', // Ya estaba incluido key: null, // No se rellena, solo se sube si el usuario selecciona cer: null, // No se rellena, solo se sube si el usuario selecciona is_importador: !!editVucem.is_importador, acusecove: !!editVucem.acusecove, acuseedocument: !!editVucem.acuseedocument, is_active: !!editVucem.is_active, }); // Resetear visibilidad de campos sensibles cuando se abre el modal de edición setShowEditPassword(false); setShowEditEfirma(false); } }, [editVucem]); // Table y header estilo Users.jsx return (
{/* Modal Relacionar Importadores */} {showRelacionarModal && selectedVucem && ( setShowRelacionarModal(false)} vucem={selectedVucem} /> )} {/* Header modernizado con gradientes azules */}

Credenciales VU

{vucemList.length} Total {vucemList.filter(v => v.is_active).length} Activos

Gestiona certificados digitales, credenciales y configuraciones Ventanilla Unica del sistema aduanero

{/* Mensaje de éxito para copiar */} {copySuccess && (
{copySuccess}
)} {/* Controles de búsqueda y filtros mejorados */}
{/* Búsqueda principal */}
setFilterUsuario(e.target.value)} autoComplete="off" /> {filterUsuario && (
)}
{/* Información y botón crear */}
{filteredList.length !== vucemList.length && (
Mostrando {filteredList.length} de {vucemList.length} registros {filterUsuario && para "{filterUsuario}"}
)}
{/* Vista responsiva: tabla para desktop, cards para mobile */} {/* Tabla para pantallas grandes */}
{loading ? ( ) : error ? ( ) : paginatedList.length > 0 ? ( <> {paginatedList.map((vucem, idx) => ( ))} {/* Rellenar con filas vacías si hay menos de 8 */} {paginatedList.length < 8 && !loading && !error && Array.from({ length: 8 - paginatedList.length }).map((_, idx) => ( ))} ) : ( )}
Usuario Password e.firma Archivos Estado Acciones
Cargando registros Ventanilla Unica...
Error: {error}
{vucem.usuario}
Patente: {vucem.patente}
{showPassword[vucem.id] ? truncateForTable(vucem.password) || '(vacío)' : '••••••••'}
{showEfirma[vucem.id] ? truncateForTable(vucem.efirma) || '(vacío)' : '••••••••'}
{vucem.key ? (
Key
) : ( Sin Key )} {vucem.cer ? (
Cer
) : ( Sin Cer )}
{vucem.is_active ? ( Activo ) : ( Inactivo )} {vucem.is_importador && ( Importador )}
 

No hay registros

Comienza creando tu primer registro de Ventanilla Unica.

{/* Cards para pantallas pequeñas */}
{loading ? (
Cargando registros...
) : error ? (
Error: {error}
) : paginatedList.length > 0 ? (
{paginatedList.map((vucem, idx) => (

{vucem.usuario}

Patente: {vucem.patente}

Password:
{showPassword[vucem.id] ? truncateForTable(vucem.password) || '(vacío)' : '••••••••'}
e.firma:
{showEfirma[vucem.id] ? truncateForTable(vucem.efirma) || '(vacío)' : '••••••••'}
Estado:
{vucem.is_active ? ( Activo ) : ( Inactivo )} {vucem.is_importador && ( Importador )}
Archivos:
{vucem.key ? (
Key
) : ( Sin Key )} {vucem.cer ? (
Cer
) : ( Sin Cer )}
))}
) : (

No hay registros

Comienza creando tu primer registro.

)}
{/* Paginación mejorada */} {totalPages > 1 && (
Mostrando {((page - 1) * pageSize) + 1} a{' '} {Math.min(page * pageSize, filteredList.length)} de{' '} {filteredList.length} registros
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => ( ))}
)} {/* Modal de creación */} {showCreateModal && (
{/* Header formal en escala de azules */}

Registro de Credencial VU

Sistema de Gestión de Credenciales Aduanales

{/* Contenido del formulario */}
{ e.preventDefault(); const formData = new FormData(); formData.append('usuario', form.usuario); formData.append('password', form.password); formData.append('patente', form.patente); formData.append('efirma', form.efirma); formData.append('is_importador', form.is_importador); formData.append('is_active', form.is_active); formData.append('acusecove', form.acusecove); formData.append('acuseedocument', form.acuseedocument); formData.append('organizacion_id', form.organizacion); if (form.key) formData.append('key', form.key); if (form.cer) formData.append('cer', form.cer); try { const res = await postFormDataWithAuth(`${API_URL}/vucem/vucem/`, formData); if (!res.ok) throw new Error('Error al crear'); await fetchVucem(); closeModals(); } catch (err) { alert('Error al crear'); } }}> {/* Sección de Información Básica */}

Información Básica del Usuario

Datos de identificación del usuario VU

{/* Sección de Credenciales */}

Credenciales de Seguridad

Información de autenticación del sistema

{/* Sección de Archivos de Certificado */}

Certificados Digitales

Archivos de certificado y clave privada

{form.key && (
{form.key.name}
)}
{form.cer && (
{form.cer.name}
)}
{/* Sección de Configuraciones */}

Configuraciones del Sistema

Opciones y permisos de la credencial

{/* Seccion de organizacion para super_users */} {currentUser.is_superuser ? (

Configuracion de Organizaciones

Asignar la Organizacion a la que pertenece.

) : (
)} {/* Botones de acción */}
)} {/* Modal de edición */} {showEditModal && editVucem && (
{/* Header formal en escala de azules */}

Editar Credencial

Modificación de Credenciales Aduanales

{/* Contenido del formulario */}
{ e.preventDefault(); if (!editVucem) return; const formData = new FormData(); formData.append('usuario', form.usuario); if (form.password) formData.append('password', form.password); formData.append('patente', form.patente); formData.append('efirma', form.efirma); if (form.key) formData.append('key', form.key); if (form.cer) formData.append('cer', form.cer); formData.append('is_importador', form.is_importador); formData.append('acusecove', form.acusecove); formData.append('acuseedocument', form.acuseedocument); formData.append('is_active', form.is_active); try { const res = await putFormDataWithAuth(`${API_URL}/vucem/vucem/${editVucem.id}/`, formData); if (!res.ok) throw new Error('Error al actualizar'); await fetchVucem(); closeModals(); } catch (err) { alert('Error al actualizar'); } }}> {/* Sección de Información Básica */}

Información Básica del Usuario

Datos de identificación del usuario VU

{/* Sección de Credenciales */}

Credenciales de Seguridad

Información de autenticación del sistema

{/* Sección de Archivos de Certificado */}

Certificados Digitales

Archivos de certificado y clave privada

{form.key && (
{form.key.name}
)}
{form.cer && (
{form.cer.name}
)}
{/* Sección de Configuraciones */}

Configuraciones del Sistema

Opciones y permisos de la credencial

{/* Botones de acción */}
)} {/* Modal de eliminación */} {showDeleteModal && deleteVucem && (

¿Eliminar Credencial?

¿Estás seguro que deseas eliminar el registro {deleteVucem.usuario}?

Esta acción no se puede deshacer.

)}
); }