import { useState, useEffect, useCallback, useRef } from "react"; import { FormsSheetApi } from "../../../api/form_sheet"; import { useRealtime } from "../contexts/RealtimeContext"; import { deleteRow, insertCell, insertProjectRow, updateCells, } from "../utils/cellUtils"; import { toast } from "react-toastify"; import { getParentId, isVspNewRow, isVspRow } from "../utils/rowUtils"; import { ProjectsApi } from "../../../api/projects"; const useRealtimeData = (formId, sheetName, direction, formType, year) => { const [data, setData] = useState([]); const [isConnected, setIsConnected] = useState(false); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); const [editingCells, setEditingCells] = useState({}); const dataRef = useRef(data); const { isConnected: wsConnected, error: wsError, subscribeToCellUpdates, subscribeToRowAdds, subscribeToProgramAdds, subscribeToProjectAdds, subscribeToRowDeletes, subscribeToErrors, subscribeToCellEditStart, subscribeToCellEditEnd, setLockedCells, } = useRealtime(); const formatedToSubRows = useCallback((depth, oldData) => { if (depth === 0) return oldData; const dataFiltered = oldData.filter((d) => d.depth < depth); for (let i = 0; i < dataFiltered.length; i++) { const curRow = dataFiltered[i]; // Находим следующий элемент на том же или более высоком уровне let nextRow = null; for (let j = i + 1; j < dataFiltered.length; j++) { if (dataFiltered[j].depth <= curRow.depth) { nextRow = dataFiltered[j]; break; } } // Находим всех потомков для текущей строки const subRows = oldData.filter((d) => { // Потомок должен иметь глубину больше чем у родителя if (d.depth <= curRow.depth) return false; // Потомок должен идти после родителя по sort_order if (d.sort_order <= curRow.sort_order) return false; // Если есть следующий элемент на том же уровне - потомок должен быть до него if (nextRow && d.sort_order >= nextRow.sort_order) return false; return true; }); if (subRows.length > 0) { curRow.subRows = subRows; } } // Рекурсивно обрабатываем следующий уровень // Находим максимальную глубину в текущем dataFiltered const maxDepth = Math.max(...dataFiltered.map((d) => d.depth)); if (maxDepth > 0) { return formatedToSubRows(maxDepth, dataFiltered); } return dataFiltered; }, []); // Загрузка начальных данных useEffect(() => { const loadInitialData = async () => { try { setIsLoading(true); let res; if (formType == "PROJECT") { res = await ProjectsApi.getTableData(formId, sheetName, year); } else if (direction !== null) { res = await FormsSheetApi.get(formId, sheetName, { params: { direction: direction }, }); } else { res = await FormsSheetApi.get(formId, sheetName); } const data = res.result; if (!data || data.length === 0) { setData([]); return; } const hierarchicalData = buildHierarchy(data); setData(hierarchicalData); } catch (err) { console.error("Failed to load initial data:", err); setError(err.message); } finally { setIsLoading(false); } }; loadInitialData(); }, [formId, sheetName]); const buildHierarchy = (items) => { const result = []; const stack = []; const getTypeLevel = (type) => { const levels = { ROOT: 0, GROUP: 1, ITEM: 2, SUB_ITEM: 3, INPUT: 4, }; return levels[type] ?? 999; }; for (const item of items) { const currentLevel = getTypeLevel(item.row_type); while ( stack.length > 0 && getTypeLevel(stack[stack.length - 1].row_type) >= currentLevel ) { stack.pop(); } const parent = stack.length > 0 ? stack[stack.length - 1] : null; const newNode = { ...item, subRows: [] }; if (parent) { if (!parent.subRows) { parent.subRows = []; } parent.subRows.push(newNode); } else { result.push(newNode); } // Добавляем текущий узел в стек, если он может иметь детей if (item.row_type !== "INPUT") { stack.push(newNode); } } return result; }; useEffect(() => { setIsConnected(wsConnected); }, [wsConnected]); useEffect(() => { setError(wsError); }, [wsError]); useEffect(() => { dataRef.current = data; }, [data]); // Подписка на обновления WebSocket useEffect(() => { if (!wsConnected) return; const unsubscribeCellUpdates = subscribeToCellUpdates((updatedCells) => { if (!updatedCells || updatedCells.length === 0) return; setData((prevData) => { const newData = updateCells(prevData, updatedCells); return newData; }); }); const unsubscribeRowAdds = subscribeToRowAdds((data) => { const updates = data.result.data; const parent_id = getParentId(data.data); const newLineId = data.result.new_line_id; const [updatedCells, newRow] = updates.reduce( (res, row) => { if (row[3]?.line_id === newLineId || isVspNewRow(row)) { res[1] = row; } else { res[0].push(row); } return res; }, [[], null], ); if (newRow === null) { toast.error("Ошибка получения данных"); return; } setData((prevData) => { const updateData = updateCells(prevData, updatedCells); const newData = insertCell(updateData, newRow, parent_id); return newData; }); }); const unsubscribeProjectAdds = subscribeToProjectAdds((data) => { const updates = data.result[1]; const program_id = data.data.program_id; const [updatedCells, newRow] = updates.reduce( (res, row) => { if (row[0] === "ITEM") { res[1] = row; } else { res[0].push(row); } return res; }, [[], null], ); if (newRow === null) { toast.error("Ошибка получения данных"); return; } setData((prevData) => { const updateData = updateCells(prevData, updatedCells); const newData = insertProjectRow(updateData, newRow, program_id); return newData; }); }); const unsubscribeProgramAdds = subscribeToProgramAdds((msg) => { let newRow = msg.result[1][0]; setData((prevData) => { const hasRoot = prevData.length > 0 && prevData[0].row_type === "ROOT"; if (hasRoot) { newRow = msg.result[1][1]; } const updateData = structuredClone(prevData); const [row_type, depth, sort_order, dataRow] = newRow; const newRowObject = { row_type: row_type, depth: depth, sort_order: sort_order, subRows: [], data: dataRow, }; if (hasRoot) { updateData[0].subRows.push(newRowObject); return updateData; } return [...updateData, newRowObject]; }); }); const unsubscribeRowDeletes = subscribeToRowDeletes((data) => { const updatedCells = data.result; const lineIdForDelete = data.data.row_id; setData((prevData) => { const updateData = updateCells(prevData, updatedCells); const newData = deleteRow(updateData, lineIdForDelete); console.log(newData); return newData; }); }); // Обработчик начала редактирования ячейки const unsubscribeCellEditStart = subscribeToCellEditStart((data) => { const dataCell = data.data; dataCell.column = "data." + dataCell.column; if (data.is_self) { setEditingCells(dataCell); return; } const { line_id, column } = dataCell; const cellKey = `${line_id}_${column}`; // Сохраняем информацию о том, кто начал редактирование setLockedCells((prev) => [...prev, cellKey]); }); // Обработчик завершения редактирования ячейки const unsubscribeCellEditEnd = subscribeToCellEditEnd((data) => { if (data.is_self) { setEditingCells(null); return; } const dataCell = data.data; dataCell.column = "data." + dataCell.column; const { line_id, column } = dataCell; const cellKey = `${line_id}_${column}`; setLockedCells((prev) => prev.filter((c) => c !== cellKey)); }); const unsubscribeErrors = subscribeToErrors((err) => { console.error("WebSocket error:", err); setError(err); setTimeout(() => setError(null), 5000); }); return () => { unsubscribeCellUpdates(); unsubscribeRowAdds(); unsubscribeProgramAdds(); unsubscribeProjectAdds(); unsubscribeRowDeletes(); unsubscribeErrors(); unsubscribeCellEditStart(); unsubscribeCellEditEnd(); }; }, [ wsConnected, subscribeToCellUpdates, subscribeToRowAdds, subscribeToRowDeletes, subscribeToErrors, subscribeToCellEditStart, subscribeToCellEditEnd, subscribeToProgramAdds, subscribeToProjectAdds, formatedToSubRows, ]); const updateData = useCallback( (newData) => { if (Array.isArray(newData) && !newData[0]?.subRows) { const maxDepth = newData.reduce( (prev, current) => { return (prev.depth || 0) > (current.depth || 0) ? prev : current; }, { depth: 0 }, ).depth; const formattedData = formatedToSubRows(maxDepth, newData); setData(formattedData); } else { setData(newData); } }, [formatedToSubRows], ); return { data, setData, isConnected, isLoading, error, editingCells, }; }; export default useRealtimeData;