feature/implementacion de SSE
This commit is contained in:
161
src/context/TaskProgressContext.jsx
Normal file
161
src/context/TaskProgressContext.jsx
Normal file
@@ -0,0 +1,161 @@
|
||||
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);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
} 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user