import { useEffect } from 'react'; import { toast } from 'react-toastify'; import { useAuth } from '../app/context/AuthProvider'; import { downloadExport, getExportFilename, startExportUpdates, subscribeExports } from '../api/exports'; import { downloadFile } from '../utils/downloadFile'; import { getApiErrorMessage } from '../utils/getApiErrorMessage'; export default function ExportJobs() { const { user, isAuthenticated } = useAuth(); useEffect(() => { if (!isAuthenticated || !user?.id) return; let stopped = false; const processing = new Set(); const handled = new Set(); const toastIds = new Set(); const key = (id) => `export-handled:${user.id}:${id}`; const wasHandled = (id) => { try { return handled.has(id) || Boolean(localStorage.getItem(key(id))); } catch { return handled.has(id); } }; const markHandled = (id) => { handled.add(id); try { localStorage.setItem(key(id), String(Date.now())); } catch { /* Memory fallback. */ } }; const showLoading = (id, message) => { toastIds.add(id); if (!toast.isActive(id)) toast.loading(message, { toastId: id, autoClose: false }); else toast.update(id, { render: message, isLoading: true, autoClose: false }); }; const observe = async (job) => { if (stopped || wasHandled(job.id) || processing.has(job.id)) return; const toastId = `export:${user.id}:${job.id}`; if (['queued', 'processing'].includes(job.status)) { showLoading(toastId, 'Идёт формирование файла…'); return; } processing.add(job.id); showLoading(toastId, 'Скачивание файла…'); try { if (job.status === 'failed') throw new Error(job.error || 'Ошибка экспорта'); const response = await downloadExport(job.id); if (stopped) return; await downloadFile(response, getExportFilename(job), 'xlsx'); markHandled(job.id); toast.update(toastId, { render: 'Файл сформирован, скачивание началось', type: 'success', isLoading: false, autoClose: 3000 }); } catch (error) { if (stopped) return; if (job.status === 'failed' || error?.response?.status === 410) markHandled(job.id); const message = await getApiErrorMessage(error, 'Не удалось скачать файл'); if (!stopped) toast.update(toastId, { render: message, type: 'error', isLoading: false, autoClose: 5000 }); } finally { processing.delete(job.id); } }; const unsubscribe = subscribeExports((jobs) => { for (const job of jobs) void observe(job); }); const stop = startExportUpdates(); return () => { stopped = true; unsubscribe(); stop(); for (const id of toastIds) toast.dismiss(id); }; }, [isAuthenticated, user?.id]); return null; }