364 lines
10 KiB
JavaScript
364 lines
10 KiB
JavaScript
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;
|