175 lines
5.2 KiB
JavaScript
175 lines
5.2 KiB
JavaScript
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
|
import { fetchNotificacionByTaskId } from '../api/notificaciones';
|
|
|
|
const STORAGE_KEY = 'efc_audit_tasks';
|
|
|
|
/**
|
|
* TaskProgressContext
|
|
*
|
|
* Gestiona las tareas de auditoría en background. Persiste en localStorage para
|
|
* sobrevivir recargas de página. Reconecta SSE automáticamente para tareas que
|
|
* quedaron en "processing" tras un refresh.
|
|
*
|
|
* Estructura de cada tarea:
|
|
* {
|
|
* task_id: string,
|
|
* label: string, // 'EDocuments', 'COVEs', etc.
|
|
* status: 'submitted' | 'processing' | 'completed' | 'failed',
|
|
* message: string,
|
|
* progress: number, // 0-100
|
|
* resultado: object | null,
|
|
* organizacion_id: string,
|
|
* started_at: ISO string,
|
|
* dismissed: boolean,
|
|
* }
|
|
*/
|
|
|
|
const TaskProgressContext = createContext(null);
|
|
|
|
export function useTaskProgress() {
|
|
const ctx = useContext(TaskProgressContext);
|
|
if (!ctx) throw new Error('useTaskProgress debe usarse dentro de TaskProgressProvider');
|
|
return ctx;
|
|
}
|
|
|
|
function loadFromStorage() {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return [];
|
|
const tasks = JSON.parse(raw);
|
|
const cutoff = Date.now() - 24 * 60 * 60 * 1000; // 24 horas
|
|
// Auto-descartar tareas "processing/submitted" con más de 24h (nunca terminaron)
|
|
return tasks.map(t => {
|
|
if (
|
|
(t.status === 'processing' || t.status === 'submitted') &&
|
|
t.started_at &&
|
|
new Date(t.started_at).getTime() < cutoff
|
|
) {
|
|
return { ...t, dismissed: true, status: 'failed', message: 'Tarea expirada' };
|
|
}
|
|
return t;
|
|
});
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function saveToStorage(tasks) {
|
|
try {
|
|
// Solo persistir las últimas 20 tareas no descartadas para no crecer indefinidamente
|
|
const toSave = tasks.slice(-20);
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave));
|
|
} catch {
|
|
// Storage lleno — ignorar
|
|
}
|
|
}
|
|
|
|
export function TaskProgressProvider({ children }) {
|
|
const [tasks, setTasksRaw] = useState(() => loadFromStorage());
|
|
// task_id que Auditor.jsx debe abrir automáticamente al montar
|
|
const [pendingOpenTaskId, setPendingOpenTaskId] = useState(null);
|
|
|
|
const setTasks = useCallback((updater) => {
|
|
setTasksRaw((prev) => {
|
|
const next = typeof updater === 'function' ? updater(prev) : updater;
|
|
saveToStorage(next);
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
// Añade una tarea nueva (cuando el usuario dispara una auditoría)
|
|
const addTask = useCallback((taskData) => {
|
|
setTasks((prev) => {
|
|
// Evitar duplicados
|
|
if (prev.find((t) => t.task_id === taskData.task_id)) return prev;
|
|
return [
|
|
...prev,
|
|
{
|
|
status: 'submitted',
|
|
message: 'Tarea enviada',
|
|
progress: 0,
|
|
resultado: null,
|
|
dismissed: false,
|
|
started_at: new Date().toISOString(),
|
|
...taskData,
|
|
},
|
|
];
|
|
});
|
|
}, [setTasks]);
|
|
|
|
// Actualiza campos de una tarea existente por task_id
|
|
const updateTask = useCallback((task_id, patch) => {
|
|
setTasks((prev) =>
|
|
prev.map((t) => (t.task_id === task_id ? { ...t, ...patch } : t))
|
|
);
|
|
}, [setTasks]);
|
|
|
|
// Descarta una tarea de la vista (no la borra de storage)
|
|
const dismissTask = useCallback((task_id) => {
|
|
setTasks((prev) =>
|
|
prev.map((t) => (t.task_id === task_id ? { ...t, dismissed: true } : t))
|
|
);
|
|
}, [setTasks]);
|
|
|
|
// Llamado desde TaskProgressCard cuando el usuario hace click en "Ver resultado"
|
|
const openTaskInAuditor = useCallback((task_id) => {
|
|
setPendingOpenTaskId(task_id);
|
|
}, []);
|
|
|
|
// Llamado desde Auditor.jsx después de consumir el pendingOpenTaskId
|
|
const clearPendingOpen = useCallback(() => {
|
|
setPendingOpenTaskId(null);
|
|
}, []);
|
|
|
|
const getTask = useCallback(
|
|
(task_id) => tasks.find((t) => t.task_id === task_id) ?? null,
|
|
[tasks]
|
|
);
|
|
|
|
// Al montar: intenta recuperar tareas que completaron mientras el cliente no estaba conectado
|
|
// (Redis TTL expirado, página cerrada, etc.). Consulta la Notificacion en DB por task_id.
|
|
useEffect(() => {
|
|
const staleTasks = loadFromStorage().filter(
|
|
(t) => !t.dismissed && t.status !== 'completed' && t.status !== 'failed'
|
|
);
|
|
if (staleTasks.length === 0) return;
|
|
|
|
staleTasks.forEach(async (task) => {
|
|
try {
|
|
const notif = await fetchNotificacionByTaskId(task.task_id);
|
|
if (notif?.datos?.resultado) {
|
|
updateTask(task.task_id, {
|
|
status: 'completed',
|
|
resultado: notif.datos.resultado,
|
|
message: notif.mensaje,
|
|
progress: 100,
|
|
});
|
|
}
|
|
} catch {
|
|
// Tarea aún en proceso o usuario no autenticado aún — ignorar
|
|
}
|
|
});
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
// Tareas visibles (no descartadas)
|
|
const visibleTasks = tasks.filter((t) => !t.dismissed);
|
|
|
|
return (
|
|
<TaskProgressContext.Provider
|
|
value={{
|
|
tasks,
|
|
visibleTasks,
|
|
addTask,
|
|
updateTask,
|
|
dismissTask,
|
|
openTaskInAuditor,
|
|
pendingOpenTaskId,
|
|
clearPendingOpen,
|
|
getTask,
|
|
}}
|
|
>
|
|
{children}
|
|
</TaskProgressContext.Provider>
|
|
);
|
|
}
|