diff --git a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx index d3682b2..5ef127f 100644 --- a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx +++ b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx @@ -12,11 +12,28 @@ const ContainerForText = styled.span` white-space: pre-wrap; `; +// Стилизованный компонент для плашки блокировки +const LockBadge = styled.div` + position: absolute; + top: 2px; + right: 2px; + background: rgba(0, 0, 0, 0.7); + color: white; + font-size: 9px; + padding: 1px 4px; + border-radius: 3px; + pointer-events: none; + z-index: 10; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + letter-spacing: 0.3px; + backdrop-filter: blur(4px); + border: 1px solid rgba(255, 255, 255, 0.1); +`; + // Функция для подсветки текста const highlightText = (text, searchQuery) => { if (!searchQuery || !text) return text; - // Экранируем спецсимволы в поисковом запросе const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(`(${escapedQuery})`, 'gi'); @@ -49,7 +66,6 @@ const formatNumber = (value) => { const num = Number(value); if (isNaN(num)) return String(value); - // Форматируем число с 1 знаком после запятой и разделением по 3 цифры return num.toLocaleString('ru-RU', { minimumFractionDigits: 1, maximumFractionDigits: 1, @@ -58,21 +74,34 @@ const formatNumber = (value) => { }; const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundColor, color, isEditable }) => { + const { lockedCells } = useRealtime(); + const cellKey = `${row.id}_${column.id}`; + + const isLocked = lockedCells.includes(cellKey); + const cellValue = cell.getValue(); let textValue = String(cellValue || ''); - // Проверяем, является ли значение числом, и форматируем его const isNumeric = !isNaN(Number(cellValue)) && cellValue !== null && cellValue !== undefined && cellValue !== ''; if (isNumeric) { textValue = formatNumber(cellValue); } - // Подсвечиваем текст с учетом поискового запроса const highlightedContent = useMemo(() => { return highlightText(textValue, globalFilter); }, [textValue, globalFilter]); - const lockedStyles = !isEditable ? { + // Базовые стили для заблокированной ячейки + const lockedStyles = isLocked ? { + border: '2px solid #e0e0e0', + backgroundColor: '#f5f5f5', + color: '#999999', + cursor: 'not-allowed', + opacity: 0.85, + } : {}; + + // Если isEditable false, добавляем дополнительные стили + const editableStyles = !isEditable ? { border: '2px solid #d3d3d3', color: '#525252', cursor: 'not-allowed', @@ -95,13 +124,19 @@ const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundC bottom: 0, border: '2px solid transparent', padding: '8px', - backgroundColor: backgroundColor, - color: color, - ...lockedStyles + backgroundColor: isLocked ? '#f5f5f5' : backgroundColor, + color: isLocked ? '#999999' : color, + ...lockedStyles, + ...editableStyles, }} - onClick={onClick} + onClick={isLocked ? undefined : onClick} > {highlightedContent} + {isLocked && ( + + 🔒 + + )} ); }); diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 51f678d..5a16d67 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -147,8 +147,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { scrollPaddingEnd: 0, }, onEditingCellChange: (cell) => { - console.log(cell, 'CELL') if (cell) { + if (editingCell) return; contextStartEditing?.(cell.row, cell.column); } else { contextEndEditing?.(editingCell.row, editingCell.column); @@ -226,7 +226,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { const table = useMaterialReactTable(tableConfig); useEffect(() => { - if (!dataEditingCells.line_id) { + if (!dataEditingCells || !dataEditingCells.line_id) { setEditingCell(null); return; } diff --git a/web/src/components/RealtimeTable/contexts/RealtimeContext.js b/web/src/components/RealtimeTable/contexts/RealtimeContext.js index b9ba118..9731607 100644 --- a/web/src/components/RealtimeTable/contexts/RealtimeContext.js +++ b/web/src/components/RealtimeTable/contexts/RealtimeContext.js @@ -7,7 +7,6 @@ import React, { useEffect, } from "react"; import { useWebSocket } from "../hooks/useWebSocket"; -import { useCellLocks } from "../hooks/useCellLocks"; import { toast } from "react-toastify"; const RealtimeContext = createContext(null); @@ -29,7 +28,8 @@ export const RealtimeProvider = ({ }) => { // Формируем 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")}/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ""}`; - + //для ячеек которые редактируются другими пользователями + const [lockedCells, setLockedCells] = useState([]); const handleMessage = useCallback((data) => { // Обработка ошибок if (data.error) { @@ -83,13 +83,6 @@ export const RealtimeProvider = ({ wsUrl, handleMessage, ); - const { - isCellLocked, - tryLockCell, - unlockCell, - getCellLockInfo, - releaseAllLocks, - } = useCellLocks(null); const onCellUpdateRef = useRef(null); const onRowAddRef = useRef(null); @@ -350,18 +343,16 @@ export const RealtimeProvider = ({ updateCell, addRow, deleteRow, - isCellLocked: (rowId, columnId) => - isCellLocked(rowId, columnId, userId), - getCellLockInfo: (rowId, columnId) => getCellLockInfo(rowId, columnId), - tryLockCell: (rowId, columnId) => tryLockCell(rowId, columnId, userId), - subscribeToCellUpdates, subscribeToRowAdds, subscribeToRowDeletes, subscribeToErrors, subscribeToAuthSuccess, subscribeToCellEditStart, subscribeToCellEditEnd, + subscribeToCellUpdates, retryLogin, + lockedCells, + setLockedCells, releaseAllLocks: () => releaseAllLocks(userId), }} > diff --git a/web/src/components/RealtimeTable/hooks/useCellLocks.js b/web/src/components/RealtimeTable/hooks/useCellLocks.js deleted file mode 100644 index 0a9d659..0000000 --- a/web/src/components/RealtimeTable/hooks/useCellLocks.js +++ /dev/null @@ -1,122 +0,0 @@ -import { useState, useCallback, useRef, useEffect } from 'react'; - -export const useCellLocks = (userId, lockTimeout = 30000) => { - const [lockedCells, setLockedCells] = useState(new Map()); - const lockTimersRef = useRef(new Map()); - - const getCellKey = (rowId, columnId) => `${rowId}_${columnId}`; - - const lockCell = useCallback((rowId, columnId, lockingUserId) => { - const cellKey = getCellKey(rowId, columnId); - - setLockedCells(prev => { - const newLocks = new Map(prev); - const existingLock = newLocks.get(cellKey); - - if (existingLock && existingLock.userId !== lockingUserId) { - return prev; // Cell already locked by another user - } - - newLocks.set(cellKey, { - userId: lockingUserId, - lockedAt: Date.now(), - rowId, - columnId - }); - - return newLocks; - }); - - return true; - }, []); - - const unlockCell = useCallback((rowId, columnId, unlockingUserId) => { - const cellKey = getCellKey(rowId, columnId); - - setLockedCells(prev => { - const newLocks = new Map(prev); - const lock = newLocks.get(cellKey); - - if (lock && lock.userId === unlockingUserId) { - newLocks.delete(cellKey); - } - - return newLocks; - }); - - // Clear timer if exists - if (lockTimersRef.current.has(cellKey)) { - clearTimeout(lockTimersRef.current.get(cellKey)); - lockTimersRef.current.delete(cellKey); - } - }, []); - - const tryLockCell = useCallback((rowId, columnId, currentUserId) => { - const cellKey = getCellKey(rowId, columnId); - const existingLock = lockedCells.get(cellKey); - - if (existingLock && existingLock.userId !== currentUserId) { - return false; // Cell is locked by another user - } - - if (!existingLock) { - lockCell(rowId, columnId, currentUserId); - - // Auto-unlock after timeout - const timer = setTimeout(() => { - unlockCell(rowId, columnId, currentUserId); - }, lockTimeout); - - lockTimersRef.current.set(cellKey, timer); - } - - return true; - }, [lockedCells, lockCell, unlockCell, lockTimeout]); - - const releaseAllLocks = useCallback((userId) => { - setLockedCells(prev => { - const newLocks = new Map(prev); - for (const [key, lock] of newLocks.entries()) { - if (lock.userId === userId) { - newLocks.delete(key); - if (lockTimersRef.current.has(key)) { - clearTimeout(lockTimersRef.current.get(key)); - lockTimersRef.current.delete(key); - } - } - } - return newLocks; - }); - }, []); - - const isCellLocked = useCallback((rowId, columnId, currentUserId) => { - const cellKey = getCellKey(rowId, columnId); - const lock = lockedCells.get(cellKey); - return lock && lock.userId !== currentUserId; - }, [lockedCells]); - - const getCellLockInfo = useCallback((rowId, columnId) => { - const cellKey = getCellKey(rowId, columnId); - return lockedCells.get(cellKey); - }, [lockedCells]); - - useEffect(() => { - return () => { - // Clear all timers on unmount - for (const timer of lockTimersRef.current.values()) { - clearTimeout(timer); - } - lockTimersRef.current.clear(); - }; - }, []); - - return { - lockedCells, - lockCell, - unlockCell, - tryLockCell, - isCellLocked, - getCellLockInfo, - releaseAllLocks - }; -}; \ No newline at end of file diff --git a/web/src/components/RealtimeTable/hooks/useRealtimeData.js b/web/src/components/RealtimeTable/hooks/useRealtimeData.js index 4aa9ac7..7ca18da 100644 --- a/web/src/components/RealtimeTable/hooks/useRealtimeData.js +++ b/web/src/components/RealtimeTable/hooks/useRealtimeData.js @@ -20,6 +20,7 @@ const useRealtimeData = (formId, sheetName, direction) => { subscribeToErrors, subscribeToCellEditStart, subscribeToCellEditEnd, + setLockedCells, } = useRealtime(); const formatedToSubRows = useCallback((depth, oldData) => { @@ -208,96 +209,28 @@ const useRealtimeData = (formId, sheetName, direction) => { // Обработчик начала редактирования ячейки const unsubscribeCellEditStart = subscribeToCellEditStart((data) => { - console.log(data); - console.log('START EDIT'); const dataCell = data.data; - dataCell.column = 'data.'+ dataCell.column; - setEditingCells(data.data) + dataCell.column = "data." + dataCell.column; + if (data.is_self) { + setEditingCells(data.data); + return; + } - // const { line_id, column } = data.data; - // const cellKey = `${line_id}_${column}`; + const { line_id, column } = dataCell; + const cellKey = `${line_id}_${column}`; - // // Сохраняем информацию о том, кто начал редактирование - // setEditingCells((prev) => ({ - // ...prev, - // [cellKey]: { - // userId: data.user_id || "unknown", - // timestamp: Date.now(), - // }, - // })); - - // // Можно также добавить визуальную индикацию в данных - // setData((prevData) => { - // // Функция для обновления статуса редактирования в данных - // const updateCellEditingStatus = (items) => { - // return items.map((item) => { - // if (item.line_id === line_id) { - // // Находим нужную колонку и добавляем флаг редактирования - // const updatedItem = { ...item }; - // if (updatedItem.columns && updatedItem.columns[column]) { - // updatedItem.columns[column] = { - // ...updatedItem.columns[column], - // isEditing: true, - // editingBy: data.user_id, - // }; - // } - // return updatedItem; - // } - // if (item.subRows) { - // return { - // ...item, - // subRows: updateCellEditingStatus(item.subRows), - // }; - // } - // return item; - // }); - // }; - // return updateCellEditingStatus(prevData); - // }); - - // console.log(`Cell ${cellKey} editing started by user ${data.user_id}`); + // Сохраняем информацию о том, кто начал редактирование + setLockedCells((prev) => [...prev, cellKey]); }); // Обработчик завершения редактирования ячейки const unsubscribeCellEditEnd = subscribeToCellEditEnd((data) => { setEditingCells(null); - const { line_id, column } = data.data; + const dataCell = data.data; + dataCell.column = "data." + dataCell.column; + const { line_id, column } = dataCell; const cellKey = `${line_id}_${column}`; - - // Удаляем информацию о редактировании - setEditingCells((prev) => { - const newState = { ...prev }; - delete newState[cellKey]; - return newState; - }); - - // Убираем визуальную индикацию в данных - setData((prevData) => { - const updateCellEditingStatus = (items) => { - return items.map((item) => { - if (item.line_id === line_id) { - const updatedItem = { ...item }; - if (updatedItem.columns && updatedItem.columns[column]) { - // Удаляем флаги редактирования - const { isEditing, editingBy, ...restColumns } = - updatedItem.columns[column]; - updatedItem.columns[column] = restColumns; - } - return updatedItem; - } - if (item.subRows) { - return { - ...item, - subRows: updateCellEditingStatus(item.subRows), - }; - } - return item; - }); - }; - return updateCellEditingStatus(prevData); - }); - - console.log(`Cell ${cellKey} editing ended`); + setLockedCells((prev) => prev.filter((c) => c !== cellKey)); }); const unsubscribeErrors = subscribeToErrors((err) => { @@ -344,24 +277,6 @@ const useRealtimeData = (formId, sheetName, direction) => { [formatedToSubRows], ); - // Функция для проверки, редактируется ли ячейка - const isCellEditing = useCallback( - (lineId, column) => { - const cellKey = `${lineId}_${column}`; - return !!editingCells[cellKey]; - }, - [editingCells], - ); - - // Функция для получения информации о том, кто редактирует ячейку - const getCellEditingInfo = useCallback( - (lineId, column) => { - const cellKey = `${lineId}_${column}`; - return editingCells[cellKey] || null; - }, - [editingCells], - ); - return { data, setData, @@ -369,8 +284,6 @@ const useRealtimeData = (formId, sheetName, direction) => { isLoading, error, editingCells, - isCellEditing, - getCellEditingInfo, }; };