364 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;