Se soluciono authenticacion
This commit is contained in:
2
.env
2
.env
@@ -1,4 +1,4 @@
|
|||||||
DEBUG_MODE=true
|
DEBUG_MODE=false
|
||||||
|
|
||||||
VITE_EFC_API_URL=https://api.efc-aduanasoft.com/api/v1
|
VITE_EFC_API_URL=https://api.efc-aduanasoft.com/api/v1
|
||||||
VITE_EFC_MICROSERVICE_URL=https://api.efc-aduanasoft.com/microservice/api/v1
|
VITE_EFC_MICROSERVICE_URL=https://api.efc-aduanasoft.com/microservice/api/v1
|
||||||
|
|||||||
@@ -27,11 +27,6 @@ function AppContent() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isAuthPage = location.pathname === '/login' || location.pathname === '/' || location.pathname === '/forgot-password' || location.pathname.startsWith('/user/password-reset-confirm/');
|
const isAuthPage = location.pathname === '/login' || location.pathname === '/' || location.pathname === '/forgot-password' || location.pathname.startsWith('/user/password-reset-confirm/');
|
||||||
|
|
||||||
console.log('🚀 AppContent renderizado');
|
|
||||||
console.log('📍 Ubicación actual:', location.pathname);
|
|
||||||
console.log('🔐 Es página de auth:', isAuthPage);
|
|
||||||
console.log('🎫 Token en localStorage:', !!localStorage.getItem('access'));
|
|
||||||
|
|
||||||
if (isAuthPage) {
|
if (isAuthPage) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export async function login(username, password) {
|
|||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ username, password }),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('API URL:', `${API_URL}/token/`);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Credenciales inválidas');
|
throw new Error('Credenciales inválidas');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,15 @@ import { Navigate } from 'react-router-dom';
|
|||||||
// Esta función verifica si el usuario está autenticado (por ejemplo, si hay un token en localStorage)
|
// Esta función verifica si el usuario está autenticado (por ejemplo, si hay un token en localStorage)
|
||||||
function isAuthenticated() {
|
function isAuthenticated() {
|
||||||
const token = localStorage.getItem('access');
|
const token = localStorage.getItem('access');
|
||||||
console.log('🔐 Verificando autenticación, token:', token ? 'presente' : 'ausente');
|
|
||||||
return !!token;
|
return !!token;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RequireAuth({ children }) {
|
export default function RequireAuth({ children }) {
|
||||||
const authenticated = isAuthenticated();
|
const authenticated = isAuthenticated();
|
||||||
console.log('🛡️ RequireAuth - usuario autenticado:', authenticated);
|
|
||||||
|
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
console.log('❌ No autenticado, redirigiendo a /login');
|
|
||||||
return <Navigate to="/login" replace />;
|
return <Navigate to="/login" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ Usuario autenticado, mostrando contenido');
|
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ const refreshToken = async () => {
|
|||||||
throw new Error('No refresh token available');
|
throw new Error('No refresh token available');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}/auth/token/refresh/`, {
|
// Intentar primero con el endpoint completo
|
||||||
|
let refreshEndpoint = `${API_URL}/token/refresh/`;
|
||||||
|
|
||||||
|
let response = await fetch(refreshEndpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -35,8 +38,23 @@ const refreshToken = async () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Si falla con 404, intentar con el endpoint alternativo
|
||||||
|
if (response.status === 404) {
|
||||||
|
refreshEndpoint = `${API_URL}/token/refresh/`;
|
||||||
|
response = await fetch(refreshEndpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
refresh: refresh
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to refresh token');
|
const errorText = await response.text();
|
||||||
|
throw new Error(`Failed to refresh token: ${response.status} - ${errorText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -62,7 +80,7 @@ const refreshToken = async () => {
|
|||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
throw error;
|
throw new Error('SESSION_EXPIRED');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,42 +108,75 @@ export const fetchWithAuth = async (url, options = {}) => {
|
|||||||
// Hacer la petición inicial
|
// Hacer la petición inicial
|
||||||
let response = await fetch(url, finalOptions);
|
let response = await fetch(url, finalOptions);
|
||||||
|
|
||||||
// Si la respuesta es 401 (Unauthorized), intentar renovar el token
|
// Si la respuesta es 401 (Unauthorized), manejar renovación de token
|
||||||
if (response.status === 401 && !isRefreshing) {
|
if (response.status === 401) {
|
||||||
isRefreshing = true;
|
if (!isRefreshing) {
|
||||||
|
isRefreshing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Renovar el token
|
// Renovar el token
|
||||||
const newToken = await refreshToken();
|
const newToken = await refreshToken();
|
||||||
|
|
||||||
// Procesar la cola de peticiones pendientes
|
// Procesar la cola de peticiones pendientes
|
||||||
processQueue(null, newToken);
|
processQueue(null, newToken);
|
||||||
|
|
||||||
// Actualizar el header de autorización y reintentar la petición original
|
// Actualizar el header de autorización y reintentar la petición original
|
||||||
finalOptions.headers['Authorization'] = `Bearer ${newToken}`;
|
finalOptions.headers['Authorization'] = `Bearer ${newToken}`;
|
||||||
response = await fetch(url, finalOptions);
|
response = await fetch(url, finalOptions);
|
||||||
|
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
// Si falla la renovación, procesar la cola con error
|
// Si falla la renovación, procesar la cola con error
|
||||||
processQueue(refreshError, null);
|
processQueue(refreshError, null);
|
||||||
throw refreshError;
|
throw refreshError;
|
||||||
} finally {
|
} finally {
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ya hay un refresh en proceso, agregar esta petición a la cola
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
failedQueue.push({
|
||||||
|
resolve: (token) => {
|
||||||
|
finalOptions.headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
fetch(url, finalOptions)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
},
|
||||||
|
reject
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si todavía hay un 401 después del intento de renovación, redirigir al login
|
// Si todavía hay un 401 después del intento de renovación, verificar si realmente es un problema de auth
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
localStorage.removeItem('access');
|
// Verificar si tenemos un token válido en localStorage
|
||||||
localStorage.removeItem('refresh');
|
const currentToken = localStorage.getItem('access');
|
||||||
localStorage.removeItem('user_id');
|
|
||||||
localStorage.removeItem('user_is_importador');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
// Intentar obtener más información del error
|
||||||
window.location.href = '/login';
|
let errorDetails = '';
|
||||||
}, 1000);
|
try {
|
||||||
|
const errorText = await response.text();
|
||||||
|
errorDetails = errorText;
|
||||||
|
} catch (e) {
|
||||||
|
// Error al leer respuesta
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error('Session expired');
|
// Si no hay token o el error indica problema de autenticación, limpiar sesión
|
||||||
|
if (!currentToken || errorDetails.includes('token') || errorDetails.includes('auth')) {
|
||||||
|
localStorage.removeItem('access');
|
||||||
|
localStorage.removeItem('refresh');
|
||||||
|
localStorage.removeItem('user_id');
|
||||||
|
localStorage.removeItem('user_is_importador');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
throw new Error('SESSION_EXPIRED');
|
||||||
|
} else {
|
||||||
|
// Si hay token pero sigue fallando, podría ser un problema del servidor
|
||||||
|
// No limpiar la sesión, dejar que el error se propague
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -185,21 +236,36 @@ export const postFormDataWithAuth = async (url, formData) => {
|
|||||||
try {
|
try {
|
||||||
let response = await fetch(url, options);
|
let response = await fetch(url, options);
|
||||||
|
|
||||||
if (response.status === 401 && !isRefreshing) {
|
if (response.status === 401) {
|
||||||
isRefreshing = true;
|
if (!isRefreshing) {
|
||||||
|
isRefreshing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newToken = await refreshToken();
|
const newToken = await refreshToken();
|
||||||
processQueue(null, newToken);
|
processQueue(null, newToken);
|
||||||
|
|
||||||
options.headers['Authorization'] = `Bearer ${newToken}`;
|
options.headers['Authorization'] = `Bearer ${newToken}`;
|
||||||
response = await fetch(url, options);
|
response = await fetch(url, options);
|
||||||
|
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
processQueue(refreshError, null);
|
processQueue(refreshError, null);
|
||||||
throw refreshError;
|
throw refreshError;
|
||||||
} finally {
|
} finally {
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ya hay un refresh en proceso, agregar esta petición a la cola
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
failedQueue.push({
|
||||||
|
resolve: (token) => {
|
||||||
|
options.headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
fetch(url, options)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
},
|
||||||
|
reject
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { fetchProcesamientoPedimentos } from '../api/procesos.ts';
|
import { fetchProcesamientoPedimentos } from '../api/procesos.ts';
|
||||||
import { postWithAuth } from '../fetchWithAuth';
|
import { postWithAuth, putWithAuth } from '../fetchWithAuth';
|
||||||
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
const API_URL = import.meta.env.VITE_EFC_API_URL;
|
||||||
const MICROSERVICE_URL = import.meta.env.VITE_EFC_MICROSERVICE_URL;
|
const MICROSERVICE_URL = import.meta.env.VITE_EFC_MICROSERVICE_URL;
|
||||||
|
|
||||||
@@ -29,10 +29,73 @@ export default function Procesos() {
|
|||||||
|
|
||||||
// Estado para loading de ejecución de servicio
|
// Estado para loading de ejecución de servicio
|
||||||
const [executingId, setExecutingId] = useState(null);
|
const [executingId, setExecutingId] = useState(null);
|
||||||
|
// Estado para loading de cambio de estado
|
||||||
|
const [changingStateId, setChangingStateId] = useState(null);
|
||||||
|
|
||||||
// Dropdown state: id del proceso abierto o null
|
// Dropdown state: id del proceso abierto o null
|
||||||
const [openDropdownId, setOpenDropdownId] = useState(null);
|
const [openDropdownId, setOpenDropdownId] = useState(null);
|
||||||
|
|
||||||
|
// Función para cambiar estado de Error a En Espera
|
||||||
|
const handlePasarAEspera = async (proc) => {
|
||||||
|
setChangingStateId(proc.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
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) {
|
||||||
|
// Intentar obtener más detalles del error
|
||||||
|
let errorText = 'Error desconocido';
|
||||||
|
try {
|
||||||
|
errorText = await res.text();
|
||||||
|
} catch (textErr) {
|
||||||
|
// Error al leer respuesta
|
||||||
|
}
|
||||||
|
throw new Error(`Error al cambiar el estado del proceso: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
alert('Estado cambiado a "En Espera" correctamente');
|
||||||
|
setOpenDropdownId(null);
|
||||||
|
|
||||||
|
// Refrescar la lista de procesos
|
||||||
|
window.location.reload();
|
||||||
|
} catch (err) {
|
||||||
|
// Crear un mensaje de error más detallado y persistente
|
||||||
|
const errorDetails = {
|
||||||
|
message: err.message,
|
||||||
|
status: err.status || 'N/A',
|
||||||
|
stack: err.stack,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Alert más detallado que permanece visible
|
||||||
|
const detailedMessage = `
|
||||||
|
🚨 ERROR DETALLADO:
|
||||||
|
⏰ Tiempo: ${new Date().toLocaleString()}
|
||||||
|
📝 Mensaje: ${err.message}
|
||||||
|
🔢 Status: ${err.status || 'N/A'}
|
||||||
|
🔍 Tipo: ${err.name || 'Error'}
|
||||||
|
|
||||||
|
📋 Copia este mensaje y compártelo para debugging.
|
||||||
|
`.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);
|
||||||
|
} else {
|
||||||
|
alert(detailedMessage);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setChangingStateId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Función para ejecutar el servicio según el tipo de proceso
|
// Función para ejecutar el servicio según el tipo de proceso
|
||||||
const handleEjecutarServicio = async (proc) => {
|
const handleEjecutarServicio = async (proc) => {
|
||||||
@@ -77,7 +140,6 @@ export default function Procesos() {
|
|||||||
alert('Servicio ejecutado correctamente');
|
alert('Servicio ejecutado correctamente');
|
||||||
setOpenDropdownId(null);
|
setOpenDropdownId(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error executing service:', err);
|
|
||||||
if (err.message === 'SESSION_EXPIRED') {
|
if (err.message === 'SESSION_EXPIRED') {
|
||||||
alert('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.');
|
alert('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.');
|
||||||
} else {
|
} else {
|
||||||
@@ -91,18 +153,25 @@ export default function Procesos() {
|
|||||||
// Cierra el dropdown si se hace click fuera
|
// Cierra el dropdown si se hace click fuera
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openDropdownId === null) return;
|
if (openDropdownId === null) return;
|
||||||
|
|
||||||
function handleClickOutside(e) {
|
function handleClickOutside(e) {
|
||||||
const el = document.getElementById(`dropdown-acciones-${openDropdownId}`);
|
// Buscar el dropdown específico que está abierto
|
||||||
if (el && !el.contains(e.target)) {
|
const dropdownContainer = document.getElementById(`dropdown-acciones-${openDropdownId}`);
|
||||||
|
|
||||||
|
// Si el click fue fuera del dropdown, cerrarlo
|
||||||
|
if (dropdownContainer && !dropdownContainer.contains(e.target)) {
|
||||||
setOpenDropdownId(null);
|
setOpenDropdownId(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Usar setTimeout para evitar que el click que abre el dropdown lo cierre inmediatamente
|
|
||||||
setTimeout(() => {
|
// Agregar el listener con un pequeño delay para evitar que se cierre inmediatamente
|
||||||
document.addEventListener('click', handleClickOutside);
|
const timeoutId = setTimeout(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside, true);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('click', handleClickOutside);
|
clearTimeout(timeoutId);
|
||||||
|
document.removeEventListener('click', handleClickOutside, true);
|
||||||
};
|
};
|
||||||
}, [openDropdownId]);
|
}, [openDropdownId]);
|
||||||
|
|
||||||
@@ -120,15 +189,11 @@ export default function Procesos() {
|
|||||||
filters['ordering'] = (sortOrder === 'desc' ? '-' : '') + sortField;
|
filters['ordering'] = (sortOrder === 'desc' ? '-' : '') + sortField;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Fetching procesos with filters:', filters);
|
|
||||||
|
|
||||||
const data = await fetchProcesamientoPedimentos(page, itemsPerPage, filters);
|
const data = await fetchProcesamientoPedimentos(page, itemsPerPage, filters);
|
||||||
|
|
||||||
console.log('Data received:', data);
|
|
||||||
setProcesos(data.results || []);
|
setProcesos(data.results || []);
|
||||||
setCount(data.count || 0);
|
setCount(data.count || 0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching procesos:', err);
|
|
||||||
if (err.message === 'SESSION_EXPIRED') {
|
if (err.message === 'SESSION_EXPIRED') {
|
||||||
setError('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.');
|
setError('Tu sesión ha expirado. Por favor, inicia sesión nuevamente.');
|
||||||
} else {
|
} else {
|
||||||
@@ -306,9 +371,10 @@ export default function Procesos() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Vista de tabla para pantallas grandes */}
|
{/* Vista de tabla para pantallas grandes */}
|
||||||
<div className="hidden lg:block overflow-x-auto bg-white rounded-2xl border border-gray-200 shadow-sm">
|
<div className="hidden lg:block overflow-x-auto bg-white rounded-2xl border border-gray-200 shadow-sm relative">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200 relative"
|
||||||
<thead className="bg-gradient-to-r from-gray-50 to-slate-50">
|
style={{ position: 'relative', zIndex: 1 }}>
|
||||||
|
<thead className="bg-gradient-to-r from-gray-50 to-slate-50 sticky top-0 z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-4 text-center text-xs font-bold text-gray-600 uppercase tracking-wider cursor-pointer select-none hover:bg-gray-100 transition-colors duration-200 rounded-tl-2xl"
|
<th className="px-4 py-4 text-center text-xs font-bold text-gray-600 uppercase tracking-wider cursor-pointer select-none hover:bg-gray-100 transition-colors duration-200 rounded-tl-2xl"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -365,7 +431,7 @@ export default function Procesos() {
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-100">
|
<tbody className="bg-white divide-y divide-gray-100 relative" style={{ position: 'relative' }}>
|
||||||
{procesos.length === 0 ? (
|
{procesos.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="text-center py-12">
|
<td colSpan={6} className="text-center py-12">
|
||||||
@@ -419,7 +485,7 @@ export default function Procesos() {
|
|||||||
: String(proc.servicio)}
|
: String(proc.servicio)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-4 text-center align-middle whitespace-nowrap">
|
<td className="px-4 py-4 text-center align-middle whitespace-nowrap">
|
||||||
<div className="relative inline-block text-left z-30" id={`dropdown-acciones-${proc.id}`}>
|
<div className="relative inline-block text-left" id={`dropdown-acciones-${proc.id}`}>
|
||||||
<button
|
<button
|
||||||
className="inline-flex justify-center items-center rounded-xl border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105 active:bg-gray-100"
|
className="inline-flex justify-center items-center rounded-xl border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200 transform hover:scale-105 active:bg-gray-100"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -431,53 +497,60 @@ export default function Procesos() {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{openDropdownId === proc.id && (
|
{openDropdownId === proc.id && (
|
||||||
<div className="absolute right-0 mt-2 w-48 rounded-xl shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-[9999] border border-gray-200">
|
<>
|
||||||
<div className="py-2">
|
{/* Overlay invisible para cerrar dropdown */}
|
||||||
<button
|
<div
|
||||||
className="flex items-center w-full text-left px-4 py-3 text-sm text-blue-700 hover:bg-blue-50 disabled:opacity-60 disabled:cursor-not-allowed transition-colors duration-200"
|
className="fixed inset-0 z-[100]"
|
||||||
onClick={() => {
|
onClick={() => setOpenDropdownId(null)}
|
||||||
handleEjecutarServicio(proc);
|
/>
|
||||||
setOpenDropdownId(null); // Cerrar dropdown después de ejecutar
|
{/* Dropdown menu */}
|
||||||
}}
|
<div className="absolute right-0 mt-2 w-56 rounded-xl shadow-2xl bg-white ring-1 ring-black ring-opacity-5 z-[200] border border-gray-200 overflow-hidden"
|
||||||
disabled={
|
style={{
|
||||||
executingId === proc.id ||
|
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
||||||
proc.estado === 2 || // Procesando
|
transform: 'translateZ(0)' // Force hardware acceleration
|
||||||
proc.estado === 3 || // Finalizado
|
}}>
|
||||||
proc.estado === 4 // Error
|
<div className="py-1" role="menu">
|
||||||
}
|
<button
|
||||||
>
|
className="flex items-center w-full text-left px-4 py-3 text-sm text-blue-700 hover:bg-blue-50 disabled:opacity-60 disabled:cursor-not-allowed transition-colors duration-200 border-b border-gray-100"
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
onClick={(e) => {
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h8m2 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" />
|
e.stopPropagation();
|
||||||
</svg>
|
handleEjecutarServicio(proc);
|
||||||
{executingId === proc.id ? 'Ejecutando...' : 'Ejecutar Servicio'}
|
setOpenDropdownId(null);
|
||||||
</button>
|
}}
|
||||||
<button
|
disabled={
|
||||||
className={`flex items-center w-full text-left px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-200 ${(proc.estado === 2 || proc.estado === 4) ? '' : ' opacity-50 cursor-not-allowed'}`}
|
executingId === proc.id ||
|
||||||
disabled={!(proc.estado === 2 || proc.estado === 4)}
|
proc.estado === 2 || // Procesando
|
||||||
onClick={() => {
|
proc.estado === 3 || // Finalizado
|
||||||
setOpenDropdownId(null); // Cerrar dropdown
|
proc.estado === 4 // Error
|
||||||
// Aquí iría la lógica para pasar a espera
|
}
|
||||||
}}
|
role="menuitem"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h8m2 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>
|
</svg>
|
||||||
Pasar a espera
|
<span className="font-medium">
|
||||||
</button>
|
{executingId === proc.id ? 'Ejecutando...' : 'Ejecutar Servicio'}
|
||||||
<button
|
</span>
|
||||||
className="flex items-center w-full text-left px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-200"
|
</button>
|
||||||
onClick={() => {
|
<button
|
||||||
setOpenDropdownId(null); // Cerrar dropdown
|
className={`flex items-center w-full text-left px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-200 ${proc.estado === 4 ? '' : ' opacity-50 cursor-not-allowed'}`}
|
||||||
// Aquí iría la lógica para editar
|
disabled={proc.estado !== 4 || changingStateId === proc.id}
|
||||||
}}
|
onClick={(e) => {
|
||||||
>
|
e.stopPropagation();
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
handlePasarAEspera(proc);
|
||||||
<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>
|
role="menuitem"
|
||||||
Editar
|
>
|
||||||
</button>
|
<svg className="w-4 h-4 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span className="font-medium">
|
||||||
|
{changingStateId === proc.id ? 'Cambiando...' : 'Pasar a espera'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -591,29 +664,18 @@ export default function Procesos() {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`flex items-center w-full text-left px-4 py-4 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-200 border-b border-gray-100 ${(proc.estado === 2 || proc.estado === 4) ? '' : ' opacity-50 cursor-not-allowed'}`}
|
className={`flex items-center w-full text-left px-4 py-4 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-200 ${proc.estado === 4 ? '' : ' opacity-50 cursor-not-allowed'}`}
|
||||||
disabled={!(proc.estado === 2 || proc.estado === 4)}
|
disabled={proc.estado !== 4 || changingStateId === proc.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpenDropdownId(null); // Cerrar dropdown
|
handlePasarAEspera(proc);
|
||||||
// Aquí iría la lógica para pasar a espera
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="font-medium">Pasar a espera</span>
|
<span className="font-medium">
|
||||||
</button>
|
{changingStateId === proc.id ? 'Cambiando...' : 'Pasar a espera'}
|
||||||
<button
|
</span>
|
||||||
className="flex items-center w-full text-left px-4 py-4 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-200"
|
|
||||||
onClick={() => {
|
|
||||||
setOpenDropdownId(null); // Cerrar dropdown
|
|
||||||
// Aquí iría la lógica para editar
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<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>
|
|
||||||
<span className="font-medium">Editar</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user