import React, { createContext, useContext, useCallback, useRef, useState, useEffect, } from "react"; import { useWebSocket } from "../hooks/useWebSocket"; import { toast } from "react-toastify"; import { getRowId } from "../utils/rowUtils"; const RealtimeContext = createContext(null); export const useRealtime = () => { const context = useContext(RealtimeContext); if (!context) { throw new Error("useRealtime must be used within RealtimeProvider"); } return context; }; export const RealtimeProvider = ({ children, formId, sheetName, direction, year, userId, isProject = false, }) => { // Формируем URL (кастомные секреты на фронте пока недоступны, делаем через REACT_APP_ROOT_PATH) const wsUrl = `${( process.env.REACT_APP_API_URL || process.env.REACT_APP_API_URL || process.env.REACT_APP_ROOT_PATH || "ws://localhost:8000" ).replace(/^http/, "ws")}` + (!isProject ? `/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ""}` : `/api/v1/ws/projects/${formId}/report/${year}/${sheetName}`); //для ячеек которые редактируются другими пользователями const [lockedCells, setLockedCells] = useState([]); const handleMessage = useCallback((data) => { // Обработка ошибок if (data.error) { console.log(data.error.message); toast.error("Server error:" + data.error.message || ""); onErrorRef.current?.(data.error); return; } switch (data.event) { case "cell_edit_start": console.log("Cell edit started:", data); if (onCellEditStartRef.current) { onCellEditStartRef.current(data); } break; case "cell_edit_end": console.log("Cell edit ended:", data); if (onCellEditEndRef.current) { onCellEditEndRef.current(data); } break; case "cell_updated": if (data.result && onCellUpdateRef.current) { const updatedCells = Array.isArray(data.result) ? data.result : [data.result]; onCellUpdateRef.current(updatedCells); } break; case "row_added": if (data.result && onRowAddRef.current) { onRowAddRef.current(data); } break; case "row_deleted": if (data.result && onRowDeleteRef.current) { onRowDeleteRef.current(data); } break; default: console.log("Unknown event:", data.event); } }, []); const { isConnectedRef, isConnected, sendMessage, disconnect, lastError } = useWebSocket( wsUrl, handleMessage, ); const onCellUpdateRef = useRef(null); const onRowAddRef = useRef(null); const onRowDeleteRef = useRef(null); const onErrorRef = useRef(null); const onAuthSuccessRef = useRef(null); const onCellEditStartRef = useRef(null); const onCellEditEndRef = useRef(null); const authAttemptedRef = useRef(false); const reconnectTimerRef = useRef(null); // Функция для получения токена из localStorage const getAccessToken = useCallback(() => { try { const token = localStorage.getItem("access_token"); if (!token) { console.warn("No access token found in localStorage"); return null; } return token; } catch (error) { console.error("Error reading token from localStorage:", error); return null; } }, []); const sendLogin = useCallback(() => { const token = getAccessToken(); if (!token) { console.error("Cannot login: no token available"); onErrorRef.current?.("Токен авторизации не найден"); return false; } const loginMessage = { event: "user_login", data: { token }, }; return sendMessage(loginMessage); }, [getAccessToken, sendMessage]); useEffect(() => { if (isConnected && !authAttemptedRef.current) { authAttemptedRef.current = true; console.log("WebSocket connected, sending login..."); setTimeout(() => { sendLogin(); }, 100); } }, [isConnected, sendLogin]); useEffect(() => { if (!isConnected) { authAttemptedRef.current = false; } }, [isConnected]); const addCommonField = (message) => { if (isProject) { message.report_type = sheetName; message.project_id = formId; message.year = year; } else { message.sheet = sheetName; message.form_id = formId; message.direction = direction; } return message; }; // Функция для начала редактирования ячейки const startEditing = useCallback( async (row, column) => { const columnId = column.id; const rowId = row.id; if (!isConnectedRef) { onErrorRef.current?.("WebSocket не подключен"); return false; } const lineId = row.id || null; const colId = columnId.slice(5); // убираем префикс "data." const message = { event: "cell_edit_start", data: { line_id: lineId, column: colId, }, }; const sent = sendMessage(message); if (!sent) { return false; } return true; }, [isConnected, sendMessage], ); // Функция для завершения редактирования ячейки const endEditing = useCallback( async (row, column) => { const columnId = column.id; const rowId = row.id; if (!isConnectedRef) { onErrorRef.current?.("WebSocket не подключен"); return false; } const lineId = row.id || null; const colId = columnId.slice(5); const message = { event: "cell_edit_end", data: { line_id: lineId, column: colId, }, }; const sent = sendMessage(message); if (!sent) { return false; } return true; }, [isConnected, sendMessage], ); const updateCell = useCallback( async (row, column, value) => { const columnId = column.id; const rowId = row.id; if (!isConnectedRef) { onErrorRef.current?.("WebSocket не подключен"); return false; } const lineId = getRowId(row); const colId = columnId.slice(5); const message = { event: "cell_updated", data: { line_id: lineId, column: colId, value: value, }, }; const sent = sendMessage(message); if (!sent) { return false; } return true; }, [isConnected, sendMessage], ); const addRow = useCallback( async (rowData) => { if (!isConnectedRef) { onErrorRef.current?.("Нет подключения или авторизации"); return false; } const message = { event: "row_added", data: rowData, }; addCommonField(message); return sendMessage(message); }, [isConnected, sendMessage, sheetName, formId, direction], ); const deleteRow = useCallback( async (rowId) => { if (!isConnectedRef) { onErrorRef.current?.("Нет подключения или авторизации"); return false; } const message = { event: "row_deleted", data: { row_id: rowId }, }; addCommonField(message); return sendMessage(message); }, [isConnected, sendMessage, sheetName, formId, direction], ); const subscribeToCellUpdates = useCallback((callback) => { onCellUpdateRef.current = callback; return () => { onCellUpdateRef.current = null; }; }, []); const subscribeToRowAdds = useCallback((callback) => { onRowAddRef.current = callback; return () => { onRowAddRef.current = null; }; }, []); const subscribeToRowDeletes = useCallback((callback) => { onRowDeleteRef.current = callback; return () => { onRowDeleteRef.current = null; }; }, []); const subscribeToErrors = useCallback((callback) => { onErrorRef.current = callback; return () => { onErrorRef.current = null; }; }, []); const subscribeToAuthSuccess = useCallback((callback) => { onAuthSuccessRef.current = callback; return () => { onAuthSuccessRef.current = null; }; }, []); const subscribeToCellEditStart = useCallback((callback) => { onCellEditStartRef.current = callback; return () => { onCellEditStartRef.current = null; }; }, []); const subscribeToCellEditEnd = useCallback((callback) => { onCellEditEndRef.current = callback; return () => { onCellEditEndRef.current = null; }; }, []); const retryLogin = useCallback(() => { if (isConnected) { authAttemptedRef.current = false; sendLogin(); } }, [isConnected, sendLogin]); return ( releaseAllLocks(userId), }} > {children} ); };