9 Commits

Author SHA1 Message Date
b262c7098f Merge pull request 'fix/remplazar nombre de var en certificados para extraer el nuevo link de minIO' (#24) from fix/replace-key-cert into main
Reviewed-on: #24
2026-05-04 15:46:17 +00:00
Dulce
1374dc22a3 fix/remplazar nombre de var en certificados para extraer el nuevo link de minIO 2026-05-04 09:14:36 -06:00
7e1d6ff05b Merge pull request 'datastage' (#23) from refactor/value-url-change into main
Reviewed-on: #23
2026-04-22 17:15:41 +00:00
Dulce
0fc3b66090 datastage 2026-04-22 09:44:25 -06:00
f8a81a4ef6 Merge pull request 'fix/boton para poder forzar el procesamiento de un pedimento que no puso ser cargado en el proceso inicial' (#22) from fix/procesar-pedimentos into main
Reviewed-on: #22
2026-04-16 13:23:38 +00:00
Dulce
b0ac0ce06b fix/boton para poder forzar el procesamiento de un pedimento que no puso ser cargado en el proceso inicial 2026-04-16 07:18:34 -06:00
80701159fb Merge pull request 'fix: se agrega como para seleccion de organizaciones para filtrar los procesos por organizacion y ejecutar los procesos por el filtro de organizacion establecido.' (#21) from T2026-01-032 into main
Reviewed-on: #21
2026-02-05 16:09:33 +00:00
b29c586524 fix: se agrega como para seleccion de organizaciones para filtrar los procesos por organizacion y ejecutar los procesos por el filtro de organizacion establecido. 2026-02-03 16:41:06 -07:00
2fd3ab5483 Merge pull request 'fix: se crea boton para ejecutar los procesos de consulta a ventanilla unica.' (#20) from req--T2025-08-098 into main
Reviewed-on: #20
2026-02-03 17:55:17 +00:00
5 changed files with 257 additions and 21 deletions

View File

@@ -39,6 +39,7 @@ export async function fetchTasks(
} }
}); });
console.log('Params:', params.toString());
const res = await fetchWithAuth(`${API_URL}/tasks/tasks/?${params.toString()}`); const res = await fetchWithAuth(`${API_URL}/tasks/tasks/?${params.toString()}`);
if (!res.ok) { if (!res.ok) {
@@ -61,6 +62,7 @@ export interface ComandoResponse {
// Interfaz para los parámetros de ejecución // Interfaz para los parámetros de ejecución
export interface EjecutarComandoParams { export interface EjecutarComandoParams {
procesamiento?: string; procesamiento?: string;
organizacionid?: string;
todos?: boolean; todos?: boolean;
} }
@@ -79,6 +81,10 @@ export async function ejecutarComando(
requestData.procesamiento = params.procesamiento; requestData.procesamiento = params.procesamiento;
} }
if (params.organizacionid !== undefined) {
requestData.organizacionid = params.organizacionid;
}
if (params.todos !== undefined) { if (params.todos !== undefined) {
requestData.todos = params.todos; requestData.todos = params.todos;
} }

View File

@@ -381,11 +381,11 @@ export default function Datastage() {
<tr key={item.id} className="hover:bg-slate-50 transition-colors"> <tr key={item.id} className="hover:bg-slate-50 transition-colors">
<td className="border px-2 py-2 text-center">{item.id}</td> <td className="border px-2 py-2 text-center">{item.id}</td>
<td className="border px-2 py-2 max-w-xs truncate"> <td className="border px-2 py-2 max-w-xs truncate">
{item.archivo ? ( {item.download_url ? (
<span className="flex items-center gap-1 text-xs text-gray-700 truncate font-mono"> <span className="flex items-center gap-1 text-xs text-gray-700 truncate font-mono">
{(() => { {(() => {
try { try {
const url = new URL(item.archivo); const url = new URL(item.download_url);
return decodeURIComponent(url.pathname.split('/').pop() || ''); return decodeURIComponent(url.pathname.split('/').pop() || '');
} catch { } catch {
return ''; return '';
@@ -399,7 +399,7 @@ export default function Datastage() {
item.id, item.id,
(() => { (() => {
try { try {
const url = new URL(item.archivo); const url = new URL(item.download_url);
return decodeURIComponent(url.pathname.split('/').pop() || ''); return decodeURIComponent(url.pathname.split('/').pop() || '');
} catch { } catch {
return ''; return '';
@@ -507,16 +507,16 @@ export default function Datastage() {
</span> </span>
</div> </div>
<div className="flex items-center gap-1 text-xs text-gray-700 break-all font-mono mb-1"> <div className="flex items-center gap-1 text-xs text-gray-700 break-all font-mono mb-1">
{item.archivo ? ( {item.download_url ? (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
{(() => { try { const url = new URL(item.archivo); return decodeURIComponent(url.pathname.split('/').pop() || ''); } catch { return ''; } })()} {(() => { try { const url = new URL(item.download_url); return decodeURIComponent(url.pathname.split('/').pop() || ''); } catch { return ''; } })()}
<button <button
type="button" type="button"
className="inline-flex items-center justify-center w-6 h-6 rounded bg-blue-100 border border-blue-200 text-blue-700 hover:bg-blue-200 hover:border-blue-300 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-400 ml-1" className="inline-flex items-center justify-center w-6 h-6 rounded bg-blue-100 border border-blue-200 text-blue-700 hover:bg-blue-200 hover:border-blue-300 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-400 ml-1"
title="Descargar archivo" title="Descargar archivo"
onClick={() => downloadDatastageFile( onClick={() => downloadDatastageFile(
item.id, item.id,
(() => { try { const url = new URL(item.archivo); return decodeURIComponent(url.pathname.split('/').pop() || ''); } catch { return ''; } })() (() => { try { const url = new URL(item.download_url); return decodeURIComponent(url.pathname.split('/').pop() || ''); } catch { return ''; } })()
)} )}
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -803,7 +803,7 @@ export default function Datastage() {
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-40"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-40">
<div className="bg-white rounded-xl shadow-2xl border border-blue-200 p-8 max-w-sm w-full flex flex-col animate-fade-in"> <div className="bg-white rounded-xl shadow-2xl border border-blue-200 p-8 max-w-sm w-full flex flex-col animate-fade-in">
<h3 className="text-lg font-bold mb-2 text-blue-900">Detalle de Datastage #{selected.id}</h3> <h3 className="text-lg font-bold mb-2 text-blue-900">Detalle de Datastage #{selected.id}</h3>
<div className="mb-1"><b>Archivo:</b> {selected.archivo ? <a href={selected.archivo} target="_blank" rel="noopener noreferrer" className="text-blue-600 underline break-all">Descargar</a> : <span className="text-gray-400">Sin archivo</span>}</div> {/* <div className="mb-1"><b>Archivo:</b> {selected.download_url ? <a href={selected.download_url} target="_blank" rel="noopener noreferrer" className="text-blue-600 underline break-all">Descargar</a> : <span className="text-gray-400">Sin archivo</span>}</div> */}
<div className="mb-1"><b>Contribuyente:</b> {selected.contribuyente}</div> <div className="mb-1"><b>Contribuyente:</b> {selected.contribuyente}</div>
<div className="mb-1"><b>Procesado:</b> <span className={selected.procesado ? 'bg-green-100 text-green-700 px-2 py-0.5 rounded-full text-xs' : 'bg-yellow-100 text-yellow-700 px-2 py-0.5 rounded-full text-xs'}>{selected.procesado ? 'Sí' : 'No'}</span></div> <div className="mb-1"><b>Procesado:</b> <span className={selected.procesado ? 'bg-green-100 text-green-700 px-2 py-0.5 rounded-full text-xs' : 'bg-yellow-100 text-yellow-700 px-2 py-0.5 rounded-full text-xs'}>{selected.procesado ? 'Sí' : 'No'}</span></div>
<div className="mb-1"><b>Organización:</b> {selected.organizacion}</div> <div className="mb-1"><b>Organización:</b> {selected.organizacion}</div>

View File

@@ -230,6 +230,125 @@ export default function Documents() {
// } // }
// // showMessage('Error durante la descarga masiva', 'error'); // // showMessage('Error durante la descarga masiva', 'error');
// }; // };
// accionar pedimento completo si no se proceso
const handleEjecutarServicio = async (pedimentoId, org) => {
try {
showMessage(`Procesando pedimento ${pedimentoId}...`, 'info');
// Construir el body de la petición
const body = {
organizacion: org, // Ajusta según tu organización, puede ser string o número
pedimento: pedimentoId.toString() // Convertir a string si es necesario
};
// Endpoint para pedimento completo
const MICROSERVICE_URL = import.meta.env.VITE_EFC_MICROSERVICE_URL;
const endpoint = `${MICROSERVICE_URL}/services/pedimento_completo`;
const response = await postWithAuth(endpoint, body);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || errorData.detail || `Error ${response.status}: ${response.statusText}`);
}
const result = await response.json();
console.log('Resultado del servicio:', result);
showMessage(`Pedimento ${pedimentoId} procesado correctamente`, 'success');
// Opcional: Refrescar la lista después de procesar
setTimeout(() => {
refetch();
}, 2000);
} catch (error) {
console.error('Error ejecutando servicio:', error);
showMessage(`Error al procesar pedimento ${pedimentoId}: ${error.message}`, 'error');
}
};
// Agrega esta función después de handleEjecutarServicio
const handleProcesarMultiplesPedimentos = async () => {
const pedimentosNoProcesados = currentDocuments.filter(ped => selectedDocuments.includes(ped.id) && !ped.existe_expediente);
if (pedimentosNoProcesados.length === 0) {
showMessage('No hay pedimentos seleccionados que estén sin procesar', 'warning');
return;
}
if (pedimentosNoProcesados.length > 200) {
showMessage(`Máximo 200 pedimentos por solicitud. Seleccionados: ${pedimentosNoProcesados.length}`, 'warning');
return;
}
try {
showMessage(`Iniciando procesamiento de ${pedimentosNoProcesados.length} pedimentos...`, 'info');
const pedimentosData = pedimentosNoProcesados.map(ped => ({
id: ped.id,
pedimento_app: ped.pedimento_app,
aduana: ped.aduana,
patente: ped.patente,
pedimento: ped.pedimento,
organizacion: ped.organizacion
}));
const MICROSERVICE_URL = import.meta.env.VITE_EFC_MICROSERVICE_URL;
const response = await postWithAuth(`${MICROSERVICE_URL}/async/services/pedimento_completo/multiple`, {
organizacion: pedimentosNoProcesados[0].organizacion.toString(),
pedimentos: pedimentosNoProcesados.map(p => p.id.toString())
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || errorData.detail || `Error ${response.status}: ${response.statusText}`);
}
const result = await response.json();
showMessage(
`Tarea iniciada: ${result.total_pedimentos} pedimentos encolados. Task ID: ${result.task_id}`,
'success'
);
setSelectedDocuments([]);
setIsSelectAll(false);
// Opcional: Iniciar polling para monitorear el progreso
// const intervalId = setInterval(async () => {
// const statusResponse = await fetchWithAuth(`${MICROSERVICE_URL}/async/task-status/${result.task_id}`);
// if (statusResponse.ok) {
// const status = await statusResponse.json();
// if (status.status === 'SUCCESS') {
// clearInterval(intervalId);
// const { success_count, failed_count, elapsed_seconds } = status.result;
// showMessage(
// `Procesamiento completado: ${success_count} exitosos, ${failed_count} fallidos. Tiempo: ${elapsed_seconds}s`,
// failed_count > 0 ? 'warning' : 'success'
// );
// refetch(); // Refrescar la lista
// } else if (status.status === 'FAILURE') {
// clearInterval(intervalId);
// showMessage(`Error en el procesamiento: ${status.message}`, 'error');
// } else if (status.status === 'PROGRESS' && status.progress) {
// const { current, total, current_pedimento, percentage } = status.progress;
// console.log(`Progreso: ${percentage}% - ${current}/${total}: ${current_pedimento}`);
// }
// }
// }, 5000);
setTimeout(() => clearInterval(intervalId), 600000);
} catch (error) {
console.error('Error procesando múltiples pedimentos:', error);
showMessage(`Error: ${error.message}`, 'error');
}
};
// Función para descargar documentos seleccionados // Función para descargar documentos seleccionados
const handleDownloadSelected = async () => { const handleDownloadSelected = async () => {
if (selectedDocuments.length === 0) { if (selectedDocuments.length === 0) {
@@ -945,6 +1064,21 @@ const downloadExpediente = async (pedimentoId, pedimentoName, setSuccess, showMe
</div> </div>
<div className="px-6 py-4"> <div className="px-6 py-4">
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{/* NUEVO BOTÓN PARA PROCESAR MÚLTIPLES */}
<button
onClick={handleProcesarMultiplesPedimentos}
className="inline-flex items-center px-4 py-2 font-medium text-white transition-colors duration-200 bg-green-600 rounded-lg shadow-sm hover:bg-green-700 hover:shadow-md"
>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Procesar seleccionados ({selectedDocuments.filter(id => {
const ped = currentDocuments.find(p => p.id === id);
return ped && !ped.existe_expediente;
}).length} pendientes)
</button>
{/* Botón existente de eliminar */}
<button <button
onClick={handleDeleteSelected} onClick={handleDeleteSelected}
className="inline-flex items-center px-4 py-2 font-medium text-white transition-colors duration-200 bg-red-600 rounded-lg shadow-sm hover:bg-red-700 hover:shadow-md" className="inline-flex items-center px-4 py-2 font-medium text-white transition-colors duration-200 bg-red-600 rounded-lg shadow-sm hover:bg-red-700 hover:shadow-md"
@@ -1145,6 +1279,21 @@ const downloadExpediente = async (pedimentoId, pedimentoName, setSuccess, showMe
</span> </span>
</td> </td>
<td className="px-4 py-3 text-center whitespace-nowrap"> <td className="px-4 py-3 text-center whitespace-nowrap">
{ped.existe_expediente ? (
<>
</>
) : (
<button
className="p-2 text-green-600 transition-colors duration-200 rounded-full hover:text-green-800 hover:bg-green-50 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
onClick={() => handleEjecutarServicio(ped.id, ped.organizacion)}
title="Procesar"
>
<svg className="w-4 h-4 text-green-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
</button>
)}
{/* handleEjecutarServicio */}
<button <button
className="p-2 text-blue-600 transition-colors duration-200 rounded-full hover:text-blue-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" className="p-2 text-blue-600 transition-colors duration-200 rounded-full hover:text-blue-800 hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
onClick={() => handleDownloadTodoElExpediente(ped.id, ped.pedimento_app)} onClick={() => handleDownloadTodoElExpediente(ped.id, ped.pedimento_app)}

View File

@@ -412,6 +412,7 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
// La API devuelve un array directamente, no un objeto con results // La API devuelve un array directamente, no un objeto con results
const credenciales = Array.isArray(data) ? data : (data.results || []); const credenciales = Array.isArray(data) ? data : (data.results || []);
console.log('credenciales >>>> ', credenciales);
return credenciales; return credenciales;
} }
return []; return [];
@@ -459,8 +460,10 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
password: credencial.password, password: credencial.password,
efirma: credencial.efirma, efirma: credencial.efirma,
// Convertir URLs completas a rutas relativas // Convertir URLs completas a rutas relativas
key: credencial.key ? credencial.key.split('/').slice(-2).join('/') : '', key: credencial.key_download_url,
cer: credencial.cer ? credencial.cer.split('/').slice(-2).join('/') : '', cer: credencial.cer_download_url,
// key: credencial.key ? credencial.key.split('/').slice(-2).join('/') : '',
// cer: credencial.cer ? credencial.cer.split('/').slice(-2).join('/') : '',
is_active: credencial.is_active, is_active: credencial.is_active,
organizacion: credencial.organizacion organizacion: credencial.organizacion
} }
@@ -2005,8 +2008,10 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
user: credencial.usuario, user: credencial.usuario,
password: credencial.password, password: credencial.password,
efirma: credencial.efirma, efirma: credencial.efirma,
key: credencial.key, key: credencial.key_download_url,
cer: credencial.cer, cer: credencial.cer_download_url,
// key: credencial.key,
// cer: credencial.cer,
is_active: credencial.is_active, is_active: credencial.is_active,
organizacion: credencial.organizacion organizacion: credencial.organizacion
} }
@@ -2078,8 +2083,10 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
user: credencial.usuario, user: credencial.usuario,
password: credencial.password, password: credencial.password,
efirma: credencial.efirma, efirma: credencial.efirma,
key: credencial.key, key: credencial.key_download_url,
cer: credencial.cer, cer: credencial.cer_download_url,
// key: credencial.key,
// cer: credencial.cer,
is_active: credencial.is_active, is_active: credencial.is_active,
organizacion: credencial.organizacion organizacion: credencial.organizacion
} }
@@ -2152,8 +2159,10 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
user: credencial.usuario, user: credencial.usuario,
password: credencial.password, password: credencial.password,
efirma: credencial.efirma, efirma: credencial.efirma,
key: credencial.key, key: credencial.key_download_url,
cer: credencial.cer, cer: credencial.cer_download_url,
// key: credencial.key,
// cer: credencial.cer,
is_active: credencial.is_active, is_active: credencial.is_active,
organizacion: credencial.organizacion organizacion: credencial.organizacion
} }
@@ -2226,8 +2235,10 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
user: credencial.usuario, user: credencial.usuario,
password: credencial.password, password: credencial.password,
efirma: credencial.efirma, efirma: credencial.efirma,
key: credencial.key, key: credencial.key_download_url,
cer: credencial.cer, cer: credencial.cer_download_url,
// key: credencial.key,
// cer: credencial.cer,
is_active: credencial.is_active, is_active: credencial.is_active,
organizacion: credencial.organizacion organizacion: credencial.organizacion
} }

View File

@@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
import { fetchTasks, ejecutarComando } from '../api/procesos.ts'; import { fetchTasks, ejecutarComando } from '../api/procesos.ts';
import { fetchWithAuth } from '../fetchWithAuth'; import { fetchWithAuth } from '../fetchWithAuth';
import { useNotification } from '../context/NotificationContext'; import { useNotification } from '../context/NotificationContext';
const API_URL = import.meta.env.VITE_EFC_API_URL;
// Modal para mostrar detalles del task // Modal para mostrar detalles del task
const TaskDetailsModal = ({ task, onClose }) => { const TaskDetailsModal = ({ task, onClose }) => {
@@ -352,6 +353,10 @@ export default function Procesos() {
const [servicioFilter, setServicioFilter] = useState(''); const [servicioFilter, setServicioFilter] = useState('');
const [statusFilter, setStatusFilter] = useState(''); const [statusFilter, setStatusFilter] = useState('');
const [organizacionFilter, setOrganizacionFilter] = useState('');
const [organizaciones, setOrganizaciones] = useState([]);
const [loadingOrganizaciones, setLoadingOrganizaciones] = useState(false);
// Sorting // Sorting
const [sortField, setSortField] = useState(''); const [sortField, setSortField] = useState('');
const [sortOrder, setSortOrder] = useState('asc'); // 'asc' | 'desc' const [sortOrder, setSortOrder] = useState('asc'); // 'asc' | 'desc'
@@ -361,6 +366,7 @@ export default function Procesos() {
pedimentoPedimentoFilter: '', pedimentoPedimentoFilter: '',
statusFilter: '', statusFilter: '',
servicioFilter: '', servicioFilter: '',
organizacionFilter: '', // Añadir esta línea
sortField: '', sortField: '',
sortOrder: 'asc' sortOrder: 'asc'
}); });
@@ -374,6 +380,7 @@ export default function Procesos() {
pedimentoPedimentoFilter, pedimentoPedimentoFilter,
servicioFilter, servicioFilter,
statusFilter, statusFilter,
organizacionFilter, // Añadir esta línea
sortField, sortField,
sortOrder sortOrder
}; };
@@ -401,6 +408,7 @@ export default function Procesos() {
if (pedimentoPedimentoFilter) filters['pedimento_app'] = pedimentoPedimentoFilter; if (pedimentoPedimentoFilter) filters['pedimento_app'] = pedimentoPedimentoFilter;
if (servicioFilter) filters['servicio'] = servicioFilter; if (servicioFilter) filters['servicio'] = servicioFilter;
if (statusFilter) filters['status'] = statusFilter; if (statusFilter) filters['status'] = statusFilter;
if (organizacionFilter) filters['organizacion'] = organizacionFilter; // Añadir esta línea
if (sortField) { if (sortField) {
// Mapear campos antiguos a nuevos si es necesario // Mapear campos antiguos a nuevos si es necesario
const fieldMapping = { const fieldMapping = {
@@ -426,17 +434,31 @@ export default function Procesos() {
} }
} }
fetchData(); fetchData();
}, [page, itemsPerPage, pedimentoPedimentoFilter, servicioFilter, statusFilter, sortField, sortOrder]); }, [page, itemsPerPage, pedimentoPedimentoFilter, servicioFilter, statusFilter, organizacionFilter, sortField, sortOrder]);
const [showProcesosDropdown, setShowProcesosDropdown] = useState(false); const [showProcesosDropdown, setShowProcesosDropdown] = useState(false);
const [ejecutandoProceso, setEjecutandoProceso] = useState(false); const [ejecutandoProceso, setEjecutandoProceso] = useState(false);
const handleEjecutarProcesamiento = async (params) => { const handleEjecutarProcesamiento = async (params) => {
// Verificar si se ha seleccionado una organización
if (!organizacionFilter) {
showMessage('Debes seleccionar una organización antes de ejecutar el proceso', 'warning');
return; // Detener la ejecución
}
try { try {
setEjecutandoProceso(true); setEjecutandoProceso(true);
setShowProcesosDropdown(false); setShowProcesosDropdown(false);
const resultado = await ejecutarComando(params); // Agregar el ID de la organización a los parámetros
const paramsConOrganizacion = {
...params,
organizacionid: organizacionFilter // Solo necesitamos el ID
};
console.log('Ejecutando proceso con parámetros:', paramsConOrganizacion);
const resultado = await ejecutarComando(paramsConOrganizacion);
if (resultado.message) { if (resultado.message) {
// Mostrar mensaje de éxito // Mostrar mensaje de éxito
@@ -481,6 +503,27 @@ useEffect(() => {
}; };
}, [showProcesosDropdown]); }, [showProcesosDropdown]);
useEffect(() => {
async function fetchOrganizaciones() {
try {
setLoadingOrganizaciones(true);
const response = await fetchWithAuth(`${API_URL}/organization/organizaciones/`);
if (response.ok) {
const data = await response.json();
setOrganizaciones(data.results || []);
}
} catch (error) {
console.error('Error al cargar organizaciones:', error);
} finally {
setLoadingOrganizaciones(false);
}
}
fetchOrganizaciones();
}, []);
return ( return (
<div className="min-h-screen p-4 bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 sm:p-6 lg:p-8"> <div className="min-h-screen p-4 bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 sm:p-6 lg:p-8">
{/* Modal de detalles del task */} {/* Modal de detalles del task */}
@@ -589,7 +632,7 @@ useEffect(() => {
</svg> </svg>
Filtros de búsqueda Filtros de búsqueda
</h3> </h3>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div className="space-y-2"> <div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold text-gray-700"> <label className="flex items-center gap-2 text-sm font-semibold text-gray-700">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div> <div className="w-2 h-2 bg-blue-500 rounded-full"></div>
@@ -653,6 +696,33 @@ useEffect(() => {
<option value="9">Acuse Cove</option> <option value="9">Acuse Cove</option>
</select> </select>
</div> </div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-sm font-semibold text-gray-700">
<div className="w-2 h-2 bg-teal-500 rounded-full"></div>
Organización
</label>
<select
className="w-full px-4 py-3 text-sm transition-all duration-200 bg-white border border-gray-300 shadow-sm rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 hover:shadow-md"
disabled={loadingOrganizaciones}
value={organizacionFilter}
onChange={e => {
setOrganizacionFilter(e.target.value);
setPage(1);
}}
>
<option value="">Todas las organizaciones</option>
{loadingOrganizaciones ? (
<option value="" disabled>Cargando organizaciones...</option>
) : (
organizaciones.map((org) => (
<option key={org.id} value={org.id}>
{org.nombre}
</option>
))
)}
</select>
</div>
</div> </div>
</div> </div>
{/* BOTÓN PARA EJECUTAR PROCESAMIENTOS - AGREGAR AQUÍ */} {/* BOTÓN PARA EJECUTAR PROCESAMIENTOS - AGREGAR AQUÍ */}