diff --git a/src/pages/Procesos.jsx b/src/pages/Procesos.jsx index bb75318..7dcd02a 100644 --- a/src/pages/Procesos.jsx +++ b/src/pages/Procesos.jsx @@ -32,13 +32,322 @@ export default function Procesos() { // Estado para loading de cambio de estado const [changingStateId, setChangingStateId] = useState(null); - // Dropdown state: id del proceso abierto o null - const [openDropdownId, setOpenDropdownId] = useState(null); + // Estado para el sistema de toast + const [toasts, setToasts] = useState([]); + + // Estado para selección masiva + const [selectedProcesos, setSelectedProcesos] = useState([]); + const [isSelectAll, setIsSelectAll] = useState(false); + + // Función para mostrar toast + const showToast = (type, title, message, details = '') => { + const id = Date.now(); + const newToast = { + id, + type, + title, + message, + details, + isVisible: true + }; + + setToasts(prev => [...prev, newToast]); + + // Auto remover el toast después de 5 segundos (8 segundos para info) + const timeout = type === 'info' ? 8000 : 5000; + setTimeout(() => { + removeToast(id); + }, timeout); + }; + + // Función para remover toast + const removeToast = (id) => { + setToasts(prev => prev.filter(toast => toast.id !== id)); + }; + + // Función para actualizar el estado de un proceso específico + const updateProcesoEstado = (procId, nuevoEstado) => { + setProcesos(prev => prev.map(proc => + proc.id === procId ? { ...proc, estado: nuevoEstado } : proc + )); + }; + + // Funciones para manejo de selección + const handleSelectProceso = (procesoId, isSelected) => { + if (isSelected) { + setSelectedProcesos(prev => [...prev, procesoId]); + } else { + setSelectedProcesos(prev => prev.filter(id => id !== procesoId)); + } + }; + + const handleSelectAll = () => { + if (isSelectAll) { + setSelectedProcesos([]); + setIsSelectAll(false); + } else { + // Solo seleccionar procesos que se pueden ejecutar (En Espera o Error) + const procesosEjecutables = procesos.filter(proc => proc.estado === 1 || proc.estado === 4); + setSelectedProcesos(procesosEjecutables.map(proc => proc.id)); + setIsSelectAll(true); + } + }; + + // Función para procesar página entera + const handleProcesarPaginaEntera = async () => { + const procesosEjecutables = procesos.filter(proc => proc.estado === 1 || proc.estado === 4); + + if (procesosEjecutables.length === 0) { + showToast('warning', 'Sin procesos ejecutables', 'No hay procesos en estado "En Espera" o "Error" para procesar.'); + return; + } + + showToast('info', 'Iniciando procesamiento masivo', `Procesando ${procesosEjecutables.length} procesos de la página actual...`); + + let exitosos = 0; + let errores = 0; + + for (const proc of procesosEjecutables) { + try { + // Cambiar estado visual a "Procesando" + updateProcesoEstado(proc.id, 2); + + // Determinar endpoint según el tipo de servicio + let endpoint = ''; + switch (proc.servicio) { + case 3: endpoint = '/services/pedimento_completo'; break; + case 4: endpoint = '/services/partidas'; break; + case 5: endpoint = '/services/remesas'; break; + case 6: endpoint = '/services/acuse'; break; + case 7: endpoint = '/services/edocument'; break; + case 8: endpoint = '/services/coves'; break; + case 9: endpoint = '/services/acuseCove'; break; + default: + updateProcesoEstado(proc.id, 4); + errores++; + continue; + } + + const body = { + pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento, + organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId, + }; + + const res = await postWithAuth(`${MICROSERVICE_URL}${endpoint}`, body); + + if (!res.ok) { + updateProcesoEstado(proc.id, 4); + errores++; + } else { + updateProcesoEstado(proc.id, 3); + exitosos++; + } + + // Pequeña pausa entre requests para no sobrecargar + await new Promise(resolve => setTimeout(resolve, 500)); + } catch (err) { + updateProcesoEstado(proc.id, 4); + errores++; + } + } + + showToast('success', 'Procesamiento masivo completado', + `Resultados: ${exitosos} exitosos, ${errores} errores de ${procesosEjecutables.length} procesos.`); + }; + + // Función para procesar seleccionados + const handleProcesarSeleccionados = async () => { + if (selectedProcesos.length === 0) { + showToast('warning', 'Sin selección', 'No hay procesos seleccionados para procesar.'); + return; + } + + const procesosAEjecutar = procesos.filter(proc => + selectedProcesos.includes(proc.id) && (proc.estado === 1 || proc.estado === 4) + ); + + if (procesosAEjecutar.length === 0) { + showToast('warning', 'Sin procesos ejecutables', 'Los procesos seleccionados no están en estado "En Espera" o "Error".'); + return; + } + + showToast('info', 'Iniciando procesamiento de seleccionados', `Procesando ${procesosAEjecutar.length} procesos seleccionados...`); + + let exitosos = 0; + let errores = 0; + + for (const proc of procesosAEjecutar) { + try { + // Cambiar estado visual a "Procesando" + updateProcesoEstado(proc.id, 2); + + // Determinar endpoint según el tipo de servicio + let endpoint = ''; + switch (proc.servicio) { + case 3: endpoint = '/services/pedimento_completo'; break; + case 4: endpoint = '/services/partidas'; break; + case 5: endpoint = '/services/remesas'; break; + case 6: endpoint = '/services/acuse'; break; + case 7: endpoint = '/services/edocument'; break; + case 8: endpoint = '/services/coves'; break; + case 9: endpoint = '/services/acuseCove'; break; + default: + updateProcesoEstado(proc.id, 4); + errores++; + continue; + } + + const body = { + pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento, + organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId, + }; + + const res = await postWithAuth(`${MICROSERVICE_URL}${endpoint}`, body); + + if (!res.ok) { + updateProcesoEstado(proc.id, 4); + errores++; + } else { + updateProcesoEstado(proc.id, 3); + exitosos++; + } + + // Pequeña pausa entre requests para no sobrecargar + await new Promise(resolve => setTimeout(resolve, 500)); + } catch (err) { + updateProcesoEstado(proc.id, 4); + errores++; + } + } + + // Limpiar selección después del procesamiento + setSelectedProcesos([]); + setIsSelectAll(false); + + showToast('success', 'Procesamiento de seleccionados completado', + `Resultados: ${exitosos} exitosos, ${errores} errores de ${procesosAEjecutar.length} procesos.`); + }; + + // Función para pasar página entera a En Espera + const handlePasarPaginaAEspera = async () => { + const procesosEnError = procesos.filter(proc => proc.estado === 4); + + if (procesosEnError.length === 0) { + showToast('warning', 'Sin procesos en error', 'No hay procesos en estado "Error" para pasar a "En Espera".'); + return; + } + + showToast('info', 'Iniciando cambio masivo de estado', `Pasando ${procesosEnError.length} procesos a "En Espera"...`); + + let exitosos = 0; + let errores = 0; + + for (const proc of procesosEnError) { + try { + // Cambiar estado visual a "Procesando" temporalmente + updateProcesoEstado(proc.id, 2); + + const body = { + estado: 1, // Cambiar a En Espera + tipo_procesamiento: 2, + pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento, + servicio: proc.servicio, + organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId + }; + + const res = await putWithAuth(`${API_URL}/customs/procesamientopedimentos/${proc.id}/`, body); + + if (!res.ok) { + // Si falla, revertir a estado Error + updateProcesoEstado(proc.id, 4); + errores++; + } else { + // Si es exitoso, cambiar estado a En Espera + updateProcesoEstado(proc.id, 1); + exitosos++; + } + + // Pequeña pausa entre requests para no sobrecargar + await new Promise(resolve => setTimeout(resolve, 300)); + } catch (err) { + updateProcesoEstado(proc.id, 4); + errores++; + } + } + + showToast('success', 'Cambio masivo de estado completado', + `Resultados: ${exitosos} exitosos, ${errores} errores de ${procesosEnError.length} procesos.`); + }; + + // Función para pasar seleccionados a En Espera + const handlePasarSeleccionadosAEspera = async () => { + if (selectedProcesos.length === 0) { + showToast('warning', 'Sin selección', 'No hay procesos seleccionados para cambiar.'); + return; + } + + const procesosACambiar = procesos.filter(proc => + selectedProcesos.includes(proc.id) && proc.estado === 4 + ); + + if (procesosACambiar.length === 0) { + showToast('warning', 'Sin procesos en error', 'Los procesos seleccionados no están en estado "Error".'); + return; + } + + showToast('info', 'Iniciando cambio de estado de seleccionados', `Pasando ${procesosACambiar.length} procesos seleccionados a "En Espera"...`); + + let exitosos = 0; + let errores = 0; + + for (const proc of procesosACambiar) { + try { + // Cambiar estado visual a "Procesando" temporalmente + updateProcesoEstado(proc.id, 2); + + const body = { + estado: 1, // Cambiar a En Espera + tipo_procesamiento: 2, + pedimento: typeof proc.pedimento === 'object' && proc.pedimento !== null ? proc.pedimento.id : proc.pedimento, + servicio: proc.servicio, + organizacion: proc.organizacion_id || proc.organizacion || proc.organizacionId + }; + + const res = await putWithAuth(`${API_URL}/customs/procesamientopedimentos/${proc.id}/`, body); + + if (!res.ok) { + // Si falla, revertir a estado Error + updateProcesoEstado(proc.id, 4); + errores++; + } else { + // Si es exitoso, cambiar estado a En Espera + updateProcesoEstado(proc.id, 1); + exitosos++; + } + + // Pequeña pausa entre requests para no sobrecargar + await new Promise(resolve => setTimeout(resolve, 300)); + } catch (err) { + updateProcesoEstado(proc.id, 4); + errores++; + } + } + + // Limpiar selección después del cambio + setSelectedProcesos([]); + setIsSelectAll(false); + + showToast('success', 'Cambio de estado de seleccionados completado', + `Resultados: ${exitosos} exitosos, ${errores} errores de ${procesosACambiar.length} procesos.`); + }; // Función para cambiar estado de Error a En Espera const handlePasarAEspera = async (proc) => { setChangingStateId(proc.id); + // Cambiar estado visual a "Procesando" inmediatamente + updateProcesoEstado(proc.id, 2); // 2 = Procesando + try { const body = { estado: 1, // Cambiar a En Espera @@ -51,7 +360,9 @@ export default function Procesos() { const res = await putWithAuth(`${API_URL}/customs/procesamientopedimentos/${proc.id}/`, body); if (!res.ok) { - // Intentar obtener más detalles del error + // Si falla, revertir a estado Error + updateProcesoEstado(proc.id, 4); // 4 = Error + let errorText = 'Error desconocido'; try { errorText = await res.text(); @@ -61,11 +372,15 @@ export default function Procesos() { throw new Error(`Error al cambiar el estado del proceso: ${errorText}`); } - alert('Estado cambiado a "En Espera" correctamente'); - setOpenDropdownId(null); + // Cambiar estado visual a "En Espera" si fue exitoso + updateProcesoEstado(proc.id, 1); // 1 = En Espera - // Refrescar la lista de procesos - window.location.reload(); + showToast('success', '¡Éxito!', 'Estado cambiado a "En Espera" correctamente'); + + // Refrescar la lista de procesos después de un delay más largo + setTimeout(() => { + window.location.reload(); + }, 3000); } catch (err) { // Crear un mensaje de error más detallado y persistente const errorDetails = { @@ -87,9 +402,9 @@ export default function Procesos() { `.trim(); if (err.message === 'SESSION_EXPIRED') { - alert('🚪 SESIÓN EXPIRADA\n\nTu sesión ha expirado. Por favor, inicia sesión nuevamente.\n\n' + detailedMessage); + showToast('error', '🚪 Sesión Expirada', 'Tu sesión ha expirado. Por favor, inicia sesión nuevamente.', detailedMessage); } else { - alert(detailedMessage); + showToast('error', '🚨 Error al cambiar estado', 'Ocurrió un error al intentar cambiar el estado del proceso.', detailedMessage); } } finally { setChangingStateId(null); @@ -100,6 +415,10 @@ export default function Procesos() { // Función para ejecutar el servicio según el tipo de proceso const handleEjecutarServicio = async (proc) => { setExecutingId(proc.id); + + // Cambiar estado visual a "Procesando" inmediatamente + updateProcesoEstado(proc.id, 2); // 2 = Procesando + let endpoint = ''; // Determinar endpoint según el tipo de servicio switch (proc.servicio) { @@ -125,7 +444,9 @@ export default function Procesos() { endpoint = '/services/acuseCove'; break; default: - alert('Servicio no soportado para ejecución directa.'); + // Revertir estado si el servicio no es soportado + updateProcesoEstado(proc.id, proc.estado); // Revertir al estado original + showToast('error', 'Servicio no soportado', 'Este servicio no es compatible para ejecución directa.'); setExecutingId(null); return; } @@ -138,46 +459,30 @@ export default function Procesos() { const res = await postWithAuth(`${MICROSERVICE_URL}${endpoint}`, body); - if (!res.ok) throw new Error('Error al ejecutar el servicio'); + if (!res.ok) { + // Si falla, cambiar estado a Error + updateProcesoEstado(proc.id, 4); // 4 = Error + throw new Error('Error al ejecutar el servicio'); + } - alert('Servicio ejecutado correctamente'); - setOpenDropdownId(null); + // Si es exitoso, cambiar estado a Finalizado + updateProcesoEstado(proc.id, 3); // 3 = Finalizado + + showToast('success', '¡Servicio ejecutado!', 'El servicio se ha ejecutado correctamente'); } catch (err) { + // Cambiar estado a Error en caso de excepción + updateProcesoEstado(proc.id, 4); // 4 = Error + if (err.message === 'SESSION_EXPIRED') { - alert('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.'); + showToast('error', '🚪 Sesión Expirada', 'Tu sesión ha expirado. Por favor, inicia sesión nuevamente.'); } else { - alert('Error al ejecutar el servicio: ' + (err instanceof Error ? err.message : String(err))); + showToast('error', '❌ Error al ejecutar servicio', 'Ocurrió un error al intentar ejecutar el servicio.', 'Error: ' + (err instanceof Error ? err.message : String(err))); } } finally { setExecutingId(null); } }; - // Cierra el dropdown si se hace click fuera - useEffect(() => { - if (openDropdownId === null) return; - - function handleClickOutside(e) { - // Buscar el dropdown específico que está abierto - const dropdownContainer = document.getElementById(`dropdown-acciones-${openDropdownId}`); - - // Si el click fue fuera del dropdown, cerrarlo - if (dropdownContainer && !dropdownContainer.contains(e.target)) { - setOpenDropdownId(null); - } - } - - // Agregar el listener con un pequeño delay para evitar que se cierre inmediatamente - const timeoutId = setTimeout(() => { - document.addEventListener('click', handleClickOutside, true); - }, 100); - - return () => { - clearTimeout(timeoutId); - document.removeEventListener('click', handleClickOutside, true); - }; - }, [openDropdownId]); - useEffect(() => { async function fetchProcesos() { setLoading(true); @@ -281,12 +586,168 @@ export default function Procesos() { Procesamiento de Pedimentos - {count > 0 && ( -
- Total de registros: - {count} +
+ {count > 0 && ( +
+ Total de registros: + {count} +
+ )} +
+
+ + {/* Botones de acción masiva */} +
+

+ + + + Acciones masivas +

+ +
+ {/* Sección de Procesamiento */} +
+
+
+

Ejecutar Servicios

+
+
+ + + +
- )} + + {/* Divisor vertical */} +
+ + {/* Sección de Cambio de Estado */} +
+
+
+

Pasar a En Espera

+
+
+ + + +
+
+ + {/* Divisor vertical */} +
+ + {/* Sección de Control */} +
+
+
+

Control

+
+ {selectedProcesos.length > 0 ? ( + + ) : ( +
+ Sin selección +
+ )} +
+
{/* Filtros responsivos mejorados */} @@ -374,12 +835,23 @@ export default function Procesos() { ) : ( <> {/* Vista de tabla para pantallas grandes */} -
+
- + {procesos.length === 0 ? ( - @@ -488,73 +969,56 @@ export default function Procesos() { : String(proc.servicio)} @@ -578,9 +1042,16 @@ export default function Procesos() { ) : ( procesos.map((proc) => ( -
+
+ handleSelectProceso(proc.id, e.target.checked)} + disabled={proc.estado === 2 || proc.estado === 3} // Deshabilitar si está Procesando o Finalizado + className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2 disabled:opacity-50 mt-1" + />
@@ -631,58 +1102,56 @@ export default function Procesos() {
-
+
+ {/* Botón Play - Ejecutar Servicio */} + + {/* Botón Reload - Pasar a Espera */} + - {openDropdownId === proc.id && ( -
-
- - -
-
- )}
)) @@ -777,6 +1246,138 @@ export default function Procesos() { )}
+ + {/* Sistema de Toast Notifications */} +
+ {toasts.map((toast) => ( +
+
+
+
+
+ {toast.type === 'success' ? ( + + + + ) : toast.type === 'error' ? ( + + + + ) : toast.type === 'warning' ? ( + + + + ) : ( + + + + )} +
+
+

{toast.title}

+

{toast.message}

+ {toast.details && ( +
+ + Ver detalles técnicos + +
+
{toast.details}
+
+
+ )} +
+ +
+
+ + {/* Barra de progreso */} +
+
+
+
+
+ ))} +
+ + {/* Estilos CSS para las animaciones */} +
); } diff --git a/src/pages/Users.jsx b/src/pages/Users.jsx index 7347a9e..d80f388 100644 --- a/src/pages/Users.jsx +++ b/src/pages/Users.jsx @@ -8,6 +8,8 @@ const initialForm = { first_name: '', last_name: '', password: '', + confirmPassword: '', + rfc: '', // Agregar campo RFC para importadores }; export default function Users() { @@ -44,6 +46,20 @@ export default function Users() { const [itemsPerPage, setItemsPerPage] = useState(10); const { showMessage } = useNotification(); + // Estados para validación de contraseña + const [passwordValidation, setPasswordValidation] = useState({ + length: false, + uppercase: false, + lowercase: false, + number: false, + special: false, + }); + const [showPasswordValidation, setShowPasswordValidation] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [passwordsMatch, setPasswordsMatch] = useState(true); + const [showPasswordMatchValidation, setShowPasswordMatchValidation] = useState(false); + const loadUsers = () => { setLoading(true); fetchUsers() @@ -107,7 +123,52 @@ export default function Users() { }, [searchTerm]); const handleChange = e => { - setForm({ ...form, [e.target.name]: e.target.value }); + const { name, value } = e.target; + setForm({ ...form, [name]: value }); + + // Validar contraseña en tiempo real + if (name === 'password') { + validatePassword(value); + // Validar coincidencia si ya hay confirmación + if (form.confirmPassword) { + validatePasswordMatch(value, form.confirmPassword); + } + } + + // Validar coincidencia de contraseñas + if (name === 'confirmPassword') { + validatePasswordMatch(form.password, value); + } + }; + + // Función para validar contraseña + const validatePassword = (password) => { + const validation = { + length: password.length >= 8, + uppercase: /[A-Z]/.test(password), + lowercase: /[a-z]/.test(password), + number: /\d/.test(password), + special: /[!@#$%^&*(),.?":{}|<>]/.test(password), + }; + setPasswordValidation(validation); + setShowPasswordValidation(password.length > 0); + }; + + // Función para validar coincidencia de contraseñas + const validatePasswordMatch = (password, confirmPassword) => { + const match = password === confirmPassword; + setPasswordsMatch(match); + setShowPasswordMatchValidation(confirmPassword.length > 0); + }; + + // Verificar si la contraseña es válida + const isPasswordValid = () => { + return Object.values(passwordValidation).every(valid => valid); + }; + + // Verificar si el formulario es válido para envío + const isFormValid = () => { + return isPasswordValid() && passwordsMatch && form.password.length > 0 && form.confirmPassword.length > 0; }; // (Eliminado duplicado de handleSubmit, ahora solo existe la versión unificada más abajo) @@ -160,6 +221,19 @@ export default function Users() { setShowEditModal(false); setShowDeleteModal(false); setUserToDelete(null); + // Resetear estados de validación de contraseña + setPasswordValidation({ + length: false, + uppercase: false, + lowercase: false, + number: false, + special: false, + }); + setShowPasswordValidation(false); + setShowPassword(false); + setShowConfirmPassword(false); + setPasswordsMatch(true); + setShowPasswordMatchValidation(false); }; // Función para normalizar texto (quitar acentos y espacios extra) @@ -981,138 +1055,329 @@ export default function Users() { {/* Modales */} {/* Modal Crear Usuario */} {showCreateModal && ( -
-
-
-
-

Crear Nuevo Usuario

+
+
+ {/* Header formal con gradiente */} +
+
+
+
+ {createType === 'importador' ? ( + + + + ) : ( + + + + )} +
+
+

+ {createType === 'importador' ? 'Registro de Nuevo Importador' : 'Registro de Nuevo Agente'} +

+

+ {createType === 'importador' ? 'Sistema de Gestión de Importadores' : 'Sistema de Gestión de Agentes Aduanales'} +

+
+
+
- -
-
-
- - + {/* Contenido del formulario */} +
+ + + {/* Sección de Información Personal */} +
+
+
+ + + +
+
+

Información Personal

+

Datos de identificación del usuario

+
- -
- - -
- - {/* Campo RFC solo para importador */} - {createType === 'importador' && ( - <> -
- - -
- {/* Campo oculto para is_importador */} - - - )} - -
- - -
- -
- - -
- -
- - +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
-
+ {/* Sección de RFC (solo para importador) */} + {createType === 'importador' && ( +
+
+
+ + + +
+
+

Información Fiscal

+

Datos fiscales del importador

+
+
+
+ + +

Formato: 12-13 caracteres (ABCD123456ABC)

+
+ +
+ )} + + {/* Sección de Seguridad */} +
+
+
+ + + +
+
+

Credenciales de Acceso

+

Configuración de seguridad de la cuenta

+
+
+
+ +
+ + +
+ + {/* Indicadores de validación de contraseña */} + {showPasswordValidation && ( +
+
+ Requisitos de contraseña: + {isPasswordValid() && ( +
+ + + + Válida +
+ )} +
+
+
+ + + + Mínimo 8 caracteres +
+
+ + + + Una letra mayúscula +
+
+ + + + Una letra minúscula +
+
+ + + + Un número +
+
+ + + + Un carácter especial (!@#$%^&*(),.?":{}|<>) +
+
+
+ )} + + {/* Campo de confirmación de contraseña */} +
+ +
+ 0 + ? 'border-green-300 focus:ring-green-500 focus:border-green-500' + : showPasswordMatchValidation && !passwordsMatch + ? 'border-red-300 focus:ring-red-500 focus:border-red-500' + : 'border-slate-300 focus:ring-red-500 focus:border-red-500' + }`} + placeholder="Confirme la contraseña" + /> + +
+ + {/* Indicador de coincidencia de contraseñas */} + {showPasswordMatchValidation && ( +
+ + + + + {passwordsMatch ? 'Las contraseñas coinciden' : 'Las contraseñas no coinciden'} + +
+ )} +
+
+
+ + {/* Botones de acción */} +
diff --git a/src/pages/Vucem.jsx b/src/pages/Vucem.jsx index ae81ed2..c7f4ecb 100644 --- a/src/pages/Vucem.jsx +++ b/src/pages/Vucem.jsx @@ -36,6 +36,10 @@ export default function Vucem() { 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 => ({ @@ -93,7 +97,7 @@ export default function Vucem() { document.body.appendChild(textArea); textArea.focus(); textArea.select(); - + try { const successful = document.execCommand('copy'); if (successful) { @@ -137,6 +141,9 @@ export default function Vucem() { // 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 @@ -158,7 +165,7 @@ export default function Vucem() { 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'); @@ -178,7 +185,7 @@ export default function Vucem() { a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); - + setCopySuccess('Certificado descargado'); setTimeout(() => setCopySuccess(''), 2000); } catch (err) { @@ -191,7 +198,7 @@ export default function Vucem() { 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'); @@ -211,7 +218,7 @@ export default function Vucem() { a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); - + setCopySuccess('Clave descargada'); setTimeout(() => setCopySuccess(''), 2000); } catch (err) { @@ -227,14 +234,14 @@ export default function Vucem() { 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'); } @@ -290,7 +297,7 @@ export default function Vucem() {
- +
@@ -421,7 +428,7 @@ export default function Vucem() {
{/* Vista responsiva: tabla para desktop, cards para mobile */} - + {/* Tabla para pantallas grandes */}
@@ -631,8 +638,8 @@ export default function Vucem() {
+ + { setSortField('id'); setSortOrder(sortField === 'id' && sortOrder === 'asc' ? 'desc' : 'asc'); @@ -437,7 +909,7 @@ export default function Procesos() {
+
@@ -452,6 +924,15 @@ export default function Procesos() { ) : ( procesos.map((proc) => (
+ +
{proc.id}
-
+
+ {/* Botón Play - Ejecutar Servicio */} + + {/* Botón Reload - Pasar a Espera */} + - {openDropdownId === proc.id && ( - <> - {/* Overlay invisible para cerrar dropdown */} -
setOpenDropdownId(null)} - /> - {/* Dropdown menu */} -
-
- - -
-
- - )}
- - -
- {/* 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 => ( - +
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map(pageNum => ( + - ))} -
- + > + {pageNum} + + ))}
+
- )} +
+ )} {/* Modal de creación */} {showCreateModal && ( -
-
-
-

Crear VUCEM

- +
+
+ {/* Header formal en escala de azules */} +
+
+
+
+ + + +
+
+

Registro de Credencial VUCEM

+

Sistema de Gestión de Credenciales Aduanales

+
+
+ +
-
{ - 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); - 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 postFormDataWithAuth(`${API_URL}/vucem/vucem/`, formData); - if (!res.ok) throw new Error('Error al crear VUCEM'); - await fetchVucem(); - closeModals(); - } catch (err) { - alert('Error al crear VUCEM'); - } - }}> -
-
- - -
-
- -
- -
- - + + {/* 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); + 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 postFormDataWithAuth(`${API_URL}/vucem/vucem/`, formData); + if (!res.ok) throw new Error('Error al crear VUCEM'); + await fetchVucem(); + closeModals(); + } catch (err) { + alert('Error al crear VUCEM'); + } + }}> + + {/* Sección de Información Básica */} +
+
+
+ + + +
+
+

Información Básica del Usuario

+

Datos de identificación del usuario VUCEM

+
+
+
+
+ + +
+
+ +
-
- - -
-
- -
- -
- - + + {/* Sección de Credenciales */} +
+
+
+ + + +
+
+

Credenciales de Seguridad

+

Información de autenticación del sistema

+
+
+
+
+ +
+ +
+ + +
+
+
+
+ +
+ +
+ + +
+
-
- - - {form.key && {form.key.name}} + + {/* Sección de Archivos de Certificado */} +
+
+
+ + + +
+
+

Certificados Digitales

+

Archivos de certificado y clave privada

+
+
+
+
+ + + {form.key && ( +
+ + + + {form.key.name} +
+ )} +
+
+ + + {form.cer && ( +
+ + + + {form.cer.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 edición */} {showEditModal && editVucem && ( -
-
-
-

Editar VUCEM

- +
+
+ {/* Header formal en escala de azules */} +
+
+
+
+ + + +
+
+

Editar Credencial VUCEM

+

Modificación de Credenciales Aduanales

+
+
+ +
-
{ - 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 VUCEM'); - await fetchVucem(); - closeModals(); - } catch (err) { - alert('Error al actualizar VUCEM'); - } - }}> -
-
- - -
-
- -
- -
- - + + {/* 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 VUCEM'); + await fetchVucem(); + closeModals(); + } catch (err) { + alert('Error al actualizar VUCEM'); + } + }}> + + {/* Sección de Información Básica */} +
+
+
+ + + +
+
+

Información Básica del Usuario

+

Datos de identificación del usuario VUCEM

+
+
+
+
+ + +
+
+ +
-
- - -
-
- -
- -
- - + + {/* Sección de Credenciales */} +
+
+
+ + + +
+
+

Credenciales de Seguridad

+

Información de autenticación del sistema

+
+
+
+
+ +
+ +
+ + +
+
+
+
+ +
+ +
+ + +
+
-
- - - {form.key && {form.key.name}} + + {/* Sección de Archivos de Certificado */} +
+
+
+ + + +
+
+

Certificados Digitales

+

Archivos de certificado y clave privada

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

Configuraciones del Sistema

+

Opciones y permisos de la credencial

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
-
- - + + {/* Botones de acción */} +
+ +
-
- - -
-
- - -
-
- - -
-
-
- - -
- + + +
)}