2026-07-14 13:51:42 +03:00

387 lines
9.5 KiB
JavaScript

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,
line_id_code: row.id,
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 (
<RealtimeContext.Provider
value={{
isConnected,
error: lastError,
startEditing,
endEditing,
updateCell,
addRow,
deleteRow,
subscribeToRowAdds,
subscribeToRowDeletes,
subscribeToErrors,
subscribeToAuthSuccess,
subscribeToCellEditStart,
subscribeToCellEditEnd,
subscribeToCellUpdates,
retryLogin,
lockedCells,
setLockedCells,
releaseAllLocks: () => releaseAllLocks(userId),
}}
>
{children}
</RealtimeContext.Provider>
);
};