Files
protoc-frontend/src/component/DualSpreadsheet/DualSpreadsheet.tsx

1004 lines
32 KiB
TypeScript
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 { useFileData, useLatestFileForTemplate } from '@/hook/useFileQueries'
import { useMultiSheetEngine } from '@/hook/useMultiSheetEngine'
import { useSheetAutoSave } from '@/hook/useSheetAutoSave'
import { produce } from 'immer'
import React, {
FC,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
// Импорты из новых модулей
import { FormulaBar, Spreadsheet } from './components'
import { useDraggableDivider } from './hooks/useDraggableDivider'
import {
CachedCellData,
CellSelection,
COL_COUNT,
DualSpreadsheetProps,
HighlightedCell,
ROW_COUNT,
SHEET_NAMES,
SHEET_TYPES,
SheetType,
} from './types'
import {
convertReferencesToHighlights,
createCellAddress,
createQualifiedCellReference,
createSelectionFromRange,
extractCellReferences,
getAllCellsInSelections,
getColumnLabel,
} from './utils'
const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
templateData = {},
mergedCells = [],
templateId,
onEngineReady,
enableAutoSave = true,
}) => {
// Мемоизируем конфигурацию листов
const sheetsConfig = useMemo(
() => [
{ name: SHEET_NAMES.report, rows: ROW_COUNT, cols: COL_COUNT },
{ name: SHEET_NAMES.calculations, rows: ROW_COUNT, cols: COL_COUNT },
],
[]
)
const { revision, updateRevision, setTemplateData, ...multiSheetEngine } =
useMultiSheetEngine({
sheets: sheetsConfig,
initialData: templateData || {},
})
// Мемоизируем объект для передачи родительскому компоненту
const engineAPI = useMemo(
() => ({
...multiSheetEngine,
updateRevision,
}),
[multiSheetEngine, updateRevision]
)
// Передаем методы движка родительскому компоненту
useEffect(() => {
if (onEngineReady) {
onEngineReady(engineAPI)
}
}, [onEngineReady, engineAPI])
const [activeSheet, setActiveSheet] = useState<SheetType>('report')
const [selectedCell, setSelectedCell] = useState<{
row: number
col: number
} | null>(null)
// Флаг, что пользователь изменил хотя бы одну ячейку
const [userEdited, setUserEdited] = useState(false)
// Состояние для множественного выделения
const [multiSelection, setMultiSelection] = useState<CellSelection[]>([])
const [isDragging, setIsDragging] = useState(false)
const [dragStart, setDragStart] = useState<{
row: number
col: number
} | null>(null)
const [isEditing, setIsEditing] = useState(false)
const [editingValue, setEditingValue] = useState<string>('') // Локальное значение во время редактирования
// Источник ввода: 'cell' (редактирование внутри ячейки) или 'formula'
const [editingSource, setEditingSource] = useState<'cell' | 'formula'>('cell')
const [isInitialized, setIsInitialized] = useState(false)
const [baseFileData, setBaseFileData] = useState<
Record<string, Record<string, any>>
>({}) // Базовые данные файла
const [highlightedCells, setHighlightedCells] = useState<HighlightedCell[]>(
[]
)
// React Query хуки для оптимизации API запросов
const { data: latestFile, isLoading: isLoadingLatestFile } =
useLatestFileForTemplate(templateId)
const { data: fileData, isLoading: isLoadingFileData } = useFileData(
latestFile?.id
)
// Мемоизируем начальное состояние ширин колонок
const initialColumnWidths = useMemo(
() => ({
report: Array(COL_COUNT).fill(96),
calculations: Array(COL_COUNT).fill(96),
}),
[]
)
const [columnWidths, setColumnWidths] =
useState<Record<SheetType, number[]>>(initialColumnWidths)
const gridRefs = useRef<Record<SheetType, any>>({
report: null,
calculations: null,
})
const containerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
report: null,
calculations: null,
})
const headerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
report: null,
calculations: null,
})
const rowHeaderRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
report: null,
calculations: null,
})
const activeCellInputRef = useRef<HTMLInputElement | null>(null)
const formulaBarRef = useRef<HTMLInputElement | null>(null)
const initialTemplateValues = useRef<Record<string, string>>({})
const cellDataCache = useRef<Record<string, Record<string, CachedCellData>>>(
{}
)
const lastRevision = useRef<number>(-1)
// Мемоизируем обработчик обновления начальных значений
const updateInitialValues = useCallback(() => {
// Обновляем начальные значения только если есть данные
const hasBaseFileData = Object.keys(baseFileData).length > 0
const hasTemplateData = Object.keys(templateData).length > 0
if (hasBaseFileData) {
Object.entries(baseFileData).forEach(([sheetName, sheetData]) => {
Object.entries(sheetData).forEach(([cellAddress, value]) => {
initialTemplateValues.current[cellAddress] = String(value || '')
})
})
} else if (hasTemplateData) {
Object.entries(templateData).forEach(([sheetName, sheetData]) => {
Object.entries(sheetData).forEach(([cellAddress, value]) => {
initialTemplateValues.current[cellAddress] = String(value || '')
})
})
}
}, [baseFileData, templateData])
useEffect(() => {
updateInitialValues()
}, [updateInitialValues])
// Мемоизируем обработчик скролла к выбранной ячейке
const scrollToSelectedCell = useCallback(() => {
if (selectedCell && activeSheet) {
const grid = gridRefs.current[activeSheet]
grid?.scrollToItem({
rowIndex: selectedCell.row,
columnIndex: selectedCell.col,
align: 'auto',
})
}
}, [selectedCell, activeSheet])
useEffect(() => {
scrollToSelectedCell()
}, [scrollToSelectedCell])
const getCachedCellData = useCallback(
(sheetType: SheetType): Record<string, CachedCellData> => {
const cacheKey = `${sheetType}-${revision}-${columnWidths[sheetType].join('-')}`
if (cellDataCache.current[cacheKey]) {
return cellDataCache.current[cacheKey]
}
const sheetName = SHEET_NAMES[sheetType]
const cachedData: Record<string, CachedCellData> = {}
const allCellValues: Record<string, string> = {}
for (let row = 0; row < ROW_COUNT; row++) {
for (let col = 0; col < COL_COUNT; col++) {
const cellKey = `${row}-${col}`
const displayValue = String(
multiSheetEngine.getCellValue(sheetName, row, col) || ''
)
allCellValues[cellKey] = displayValue.trim()
}
}
// 1. Находим ближайшую занятую колонку справа для каждой ячейки строки
const nextContentInRow: Record<number, number[]> = {}
for (let row = 0; row < ROW_COUNT; row++) {
nextContentInRow[row] = Array(COL_COUNT).fill(COL_COUNT)
let next = COL_COUNT
for (let col = COL_COUNT - 1; col >= 0; col--) {
const cellKey = `${row}-${col}`
nextContentInRow[row][col] = next // сохранить для использования ниже
if (allCellValues[cellKey]) {
next = col // текущая колонка занята → "новый" предел
}
}
}
// 2. Заполняем cache
for (let row = 0; row < ROW_COUNT; row++) {
for (let col = 0; col < COL_COUNT; col++) {
const cellKey = `${row}-${col}`
const rawValue = multiSheetEngine.getCellRawValue(sheetName, row, col)
const displayValue = allCellValues[cellKey]
const nextCol = nextContentInRow[row][col]
// ширина от текущей колонки до nextCol-1 включительно
const availablePx = columnWidths[sheetType]
.slice(col, nextCol)
.reduce((sum, w) => sum + w, 0)
const cellName = getColumnLabel(col) + (row + 1)
const baseFileValue = initialTemplateValues.current[cellName] ?? '' // Базовое значение из файла
// Нормализуем значения для сравнения
const normalizeValue = (value: any): string => {
if (value === null || value === undefined) return ''
const str = String(value).trim()
// Считаем "0" и пустую строку эквивалентными
if (str === '0' || str === '') return ''
return str
}
const normalizedBaseFile = normalizeValue(baseFileValue)
const normalizedRaw = normalizeValue(rawValue)
const isModified =
sheetType === 'report' && normalizedRaw !== normalizedBaseFile
cachedData[cellKey] = {
displayValue,
rawValue,
isModified,
nextContentCol: nextCol,
availablePx,
}
}
}
if (lastRevision.current !== revision) {
Object.keys(cellDataCache.current).forEach(key => {
if (!key.endsWith(`-${revision}`)) {
delete cellDataCache.current[key]
}
})
lastRevision.current = revision
}
cellDataCache.current[cacheKey] = cachedData
return cachedData
},
[revision, multiSheetEngine, initialTemplateValues, columnWidths]
)
// Функция для обновления подсветки ячеек при вводе формулы
const updateCellHighlights = useCallback(
(formula: string) => {
if (!formula.startsWith('=')) {
setHighlightedCells([])
return
}
try {
// Извлекаем ссылки на ячейки из формулы
const references = extractCellReferences(formula)
// Конвертируем ссылки в подсвеченные ячейки
const highlights = convertReferencesToHighlights(
references,
activeSheet
)
setHighlightedCells(highlights)
} catch (error) {
console.error('Ошибка при обновлении подсветки ячеек:', error)
setHighlightedCells([])
}
},
[activeSheet]
)
const formulaBarValue = useMemo(() => {
if (isEditing) {
return editingValue
}
if (selectedCell) {
const sheetName = SHEET_NAMES[activeSheet]
return multiSheetEngine.getCellRawValue(
sheetName,
selectedCell.row,
selectedCell.col
)
}
return ''
}, [
isEditing,
editingValue,
selectedCell,
activeSheet,
multiSheetEngine,
revision,
])
useEffect(() => {
if (isEditing && activeCellInputRef.current) {
const input = activeCellInputRef.current
input.focus()
const length = input.value.length
input.setSelectionRange(length, length)
}
}, [isEditing])
// Синхронизация между formula bar и ячейкой при редактировании
useEffect(() => {
if (isEditing && formulaBarRef.current) {
formulaBarRef.current.value = editingValue
}
}, [editingValue, isEditing])
// Обновление подсветки ячеек при вводе формулы
useEffect(() => {
if (isEditing) {
updateCellHighlights(editingValue)
} else {
// Очищаем подсветку когда не редактируем
setHighlightedCells([])
}
}, [isEditing, editingValue, updateCellHighlights])
const startEditing = useCallback(() => {
if (!selectedCell) return
// Сбрасываем множественное выделение при начале редактирования
setMultiSelection([])
const sheetName = SHEET_NAMES[activeSheet]
const rawValue = multiSheetEngine.getCellRawValue(
sheetName,
selectedCell.row,
selectedCell.col
)
setIsEditing(true)
setEditingValue(rawValue)
}, [activeSheet, selectedCell, multiSheetEngine])
const stopEditing = useCallback(
(save: boolean) => {
if (save && selectedCell) {
const sheetName = SHEET_NAMES[activeSheet]
multiSheetEngine.setCellValueWithoutRecalc(
sheetName,
selectedCell.row,
selectedCell.col,
editingValue
)
updateRevision()
multiSheetEngine.debouncedRecalc()
}
setIsEditing(false)
setEditingValue('')
setEditingSource('cell')
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
},
[activeSheet, selectedCell, editingValue, multiSheetEngine, updateRevision]
)
const handleCellChange = useCallback(
(newValue: string) => {
if (isEditing) {
setEditingValue(newValue)
if (!userEdited) setUserEdited(true)
}
},
[isEditing, userEdited]
)
const handleClearCell = useCallback(() => {
if (!selectedCell) return
const sheetName = SHEET_NAMES[activeSheet]
multiSheetEngine.setCellValueWithoutRecalc(
sheetName,
selectedCell.row,
selectedCell.col,
''
)
setUserEdited(true)
updateRevision()
multiSheetEngine.debouncedRecalc()
setMultiSelection([])
}, [activeSheet, selectedCell, multiSheetEngine, updateRevision])
const moveSelection = useCallback(
(deltaRow: number, deltaCol: number) => {
if (!selectedCell) {
setSelectedCell({ row: 0, col: 0 })
return
}
if (isEditing) {
stopEditing(true)
}
const newRow = Math.max(
0,
Math.min(ROW_COUNT - 1, selectedCell.row + deltaRow)
)
const newCol = Math.max(
0,
Math.min(COL_COUNT - 1, selectedCell.col + deltaCol)
)
setSelectedCell({ row: newRow, col: newCol })
// Сбрасываем множественное выделение при навигации
setMultiSelection([])
},
[selectedCell, isEditing, stopEditing]
)
// Функция для очистки множества выделенных ячеек
const handleClearMultipleCells = useCallback(() => {
if (multiSelection.length === 0) return
const sheetName = SHEET_NAMES[activeSheet]
const cellsToDelete = getAllCellsInSelections(multiSelection)
cellsToDelete.forEach(({ row, col }) => {
multiSheetEngine.setCellValueWithoutRecalc(sheetName, row, col, '')
})
setUserEdited(true)
updateRevision()
multiSheetEngine.debouncedRecalc()
setMultiSelection([])
}, [multiSelection, activeSheet, multiSheetEngine, updateRevision])
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (isEditing) return
if (!selectedCell && e.key.match(/^(Arrow|Enter|F2|Delete|Backspace)$/)) {
moveSelection(0, 0)
e.preventDefault()
return
}
if (!selectedCell) return
const keyHandlers: Record<string, () => void> = {
ArrowUp: () => moveSelection(-1, 0),
ArrowDown: () => moveSelection(1, 0),
ArrowLeft: () => moveSelection(0, -1),
ArrowRight: () => moveSelection(0, 1),
Enter: () => {
setEditingSource('cell')
startEditing()
},
F2: () => {
setEditingSource('cell')
startEditing()
},
Delete: () => {
// Если есть множественное выделение, удаляем все выделенные ячейки
if (multiSelection.length > 0) {
handleClearMultipleCells()
} else {
handleClearCell()
}
},
Backspace: () => {
// Если есть множественное выделение, удаляем все выделенные ячейки
if (multiSelection.length > 0) {
handleClearMultipleCells()
} else {
handleClearCell()
}
},
Escape: () => {
// Сбрасываем множественное выделение
setMultiSelection([])
},
}
// Ctrl+A - выделить все ячейки
if (e.key === 'a' && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
const allSelection = createSelectionFromRange(
0,
0,
ROW_COUNT - 1,
COL_COUNT - 1
)
setMultiSelection([allSelection])
return
}
if (keyHandlers[e.key]) {
e.preventDefault()
keyHandlers[e.key]()
} else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
e.preventDefault()
setEditingSource('cell')
setEditingValue(e.key)
setIsEditing(true)
if (!userEdited) setUserEdited(true)
}
},
[
selectedCell,
isEditing,
moveSelection,
startEditing,
handleClearCell,
multiSelection,
handleClearMultipleCells,
]
)
const handleCellClick = useCallback(
(row: number, col: number, event?: React.MouseEvent) => {
if (isEditing) {
stopEditing(true)
}
// Если удерживается Ctrl (или Cmd на Mac), добавляем к множественному выделению
if (event && (event.ctrlKey || event.metaKey)) {
const newSelection = createSelectionFromRange(row, col, row, col)
setMultiSelection(prev => [...prev, newSelection])
setSelectedCell({ row, col })
return
}
// Если удерживается Shift, расширяем выделение
if (event && event.shiftKey && selectedCell) {
const newSelection = createSelectionFromRange(
selectedCell.row,
selectedCell.col,
row,
col
)
setMultiSelection([newSelection])
return
}
// Обычный клик - очищаем множественное выделение
setMultiSelection([])
setSelectedCell({ row, col })
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
},
[isEditing, stopEditing, activeSheet, selectedCell]
)
// Новый обработчик для начала drag selection
const handleCellMouseDown = useCallback(
(row: number, col: number, event: React.MouseEvent) => {
// Игнорируем правый клик
if (event.button !== 0) return
// Если удерживаются модификаторы, не начинаем drag
if (event.ctrlKey || event.metaKey || event.shiftKey) return
setIsDragging(true)
setDragStart({ row, col })
setMultiSelection([])
setSelectedCell({ row, col })
},
[]
)
// Обработчик для drag selection
const handleCellMouseEnter = useCallback(
(row: number, col: number) => {
if (!isDragging || !dragStart) return
const newSelection = createSelectionFromRange(
dragStart.row,
dragStart.col,
row,
col
)
setMultiSelection([newSelection])
},
[isDragging, dragStart]
)
// Обработчик завершения drag selection
const handleCellMouseUp = useCallback(() => {
if (isDragging) {
setIsDragging(false)
setDragStart(null)
}
}, [isDragging])
// Глобальный обработчик для завершения drag selection
useEffect(() => {
const handleGlobalMouseUp = () => {
if (isDragging) {
setIsDragging(false)
setDragStart(null)
}
}
if (isDragging) {
document.addEventListener('mouseup', handleGlobalMouseUp)
return () => {
document.removeEventListener('mouseup', handleGlobalMouseUp)
}
}
}, [isDragging])
// Новый обработчик для кликов в режиме редактирования - вставляет ссылку
const handleCellReferenceClick = useCallback(
(targetSheetType: SheetType, row: number, col: number) => {
if (!isEditing || !selectedCell || !editingValue.startsWith('=')) {
stopEditing(true)
setSelectedCell({ row, col })
return
}
let cellReference: string
if (targetSheetType !== activeSheet) {
cellReference = createQualifiedCellReference(targetSheetType, row, col)
} else {
cellReference = createCellAddress(row, col)
}
const newValue = editingValue + cellReference
setEditingValue(newValue)
if (!userEdited) setUserEdited(true)
if (editingSource === 'formula') {
if (formulaBarRef.current) {
formulaBarRef.current.value = newValue
formulaBarRef.current.focus()
const length = newValue.length
formulaBarRef.current.setSelectionRange(length, length)
}
} else {
if (activeCellInputRef.current) {
activeCellInputRef.current.value = newValue
activeCellInputRef.current.focus({ preventScroll: true })
const length = newValue.length
activeCellInputRef.current.setSelectionRange(length, length)
}
}
},
[isEditing, selectedCell, editingValue, activeSheet, editingSource]
)
const handleCellDoubleClick = useCallback(
(row: number, col: number) => {
setSelectedCell({ row, col })
setEditingSource('cell')
startEditing()
},
[startEditing]
)
const handleColumnResize = useCallback(
(sheetType: SheetType, colIndex: number, newWidth: number) => {
setColumnWidths(
produce(draft => {
draft[sheetType][colIndex] = Math.max(60, newWidth)
const grid = gridRefs.current[sheetType]
if (grid) {
grid.resetAfterColumnIndex(colIndex)
}
})
)
},
[]
)
const handleFormulaBarChange = useCallback(
(newValue: string) => {
if (selectedCell) {
if (!isEditing) setIsEditing(true)
setEditingValue(newValue)
if (!userEdited) setUserEdited(true)
}
},
[selectedCell, isEditing, userEdited]
)
const handleFormulaBarFocus = useCallback(() => {
if (selectedCell && !isEditing) {
setEditingSource('formula')
startEditing()
}
}, [selectedCell, isEditing, startEditing])
const handleFormulaBarBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
// Проверяем, не переходит ли фокус в ячейку редактирования
const relatedTarget = e.relatedTarget as HTMLElement
if (relatedTarget && relatedTarget.closest('input[type="text"]')) {
return // Не завершаем редактирование
}
// Завершаем редактирование только если фокус ушел полностью
if (isEditing) {
stopEditing(true)
}
},
[isEditing, stopEditing]
)
const handleFormulaBarKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault()
stopEditing(true)
moveSelection(1, 0)
setTimeout(() => {
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
}, 0)
} else if (e.key === 'Escape') {
e.preventDefault()
stopEditing(false)
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
}
},
[stopEditing, moveSelection, activeSheet]
)
const onGridScroll = useCallback(
(
sheetType: SheetType,
{ scrollLeft, scrollTop }: { scrollLeft: number; scrollTop: number }
) => {
if (headerRefs.current[sheetType]) {
headerRefs.current[sheetType]!.scrollLeft = scrollLeft
}
if (rowHeaderRefs.current[sheetType]) {
rowHeaderRefs.current[sheetType]!.scrollTop = scrollTop
}
},
[]
)
// Мемоизируем состояние ширин таблиц
const [spreadsheetWidths, setSpreadsheetWidths] = useState({
L: 0.5,
R: 0.5,
})
// Мемоизируем опции автосохранения
const autoSaveOptions = useMemo(
() => ({
templateId,
autoSaveDelay: 4000,
enableAutoSave: enableAutoSave && userEdited,
}),
[templateId, enableAutoSave, userEdited]
)
// Мемоизируем getAllModifiedCells для предотвращения частых ререндеров хука автосохранения
const memoizedGetAllModifiedCells = useCallback(
multiSheetEngine.getAllModifiedCells,
[revision] // Используем revision вместо функции для более стабильной мемоизации
)
// Используем новый хук автосохранения
const {
isSaving,
isLoading,
hasUnsavedChanges,
lastSaveError,
manualSave,
clearError,
loadSheets,
} = useSheetAutoSave(
memoizedGetAllModifiedCells,
multiSheetEngine.isCalculating,
revision,
autoSaveOptions
)
// Используем хук для перетаскивания разделителя
const handleDividerDrag = useCallback((newLeftPercentage: number) => {
// Используем requestAnimationFrame для более плавного обновления
requestAnimationFrame(() => {
setSpreadsheetWidths({
L: newLeftPercentage,
R: 1 - newLeftPercentage,
})
})
}, [])
const separatorRef = useDraggableDivider(handleDividerDrag)
// Мемоизируем функцию инициализации данных
const initializeData = useCallback(async () => {
if (!templateId || isInitialized) return
let finalBaseData: Record<string, Record<string, any>> = {}
// 1. ИСПОЛЬЗУЕМ ДАННЫЕ ИЗ REACT QUERY КЕША
if (fileData && Object.keys(fileData.data).length > 0) {
finalBaseData = { [SHEET_NAMES.report]: fileData.data }
setBaseFileData(finalBaseData)
setTemplateData(finalBaseData)
}
// 2. ИСПОЛЬЗУЕМ templateData КАК FALLBACK или основные данные
const hasTemplateData =
Object.keys(templateData).length > 0 &&
Object.values(templateData).some(sheet => Object.keys(sheet).length > 0)
if (hasTemplateData && Object.keys(finalBaseData).length === 0) {
finalBaseData = templateData
setBaseFileData(finalBaseData)
setTemplateData(templateData)
}
// 3. ЗАГРУЖАЕМ ПОЛЬЗОВАТЕЛЬСКИЕ ИЗМЕНЕНИЯ ИЗ API
try {
const apiSheets = await loadSheets()
// Преобразуем листы API в формат данных
const userChanges: Record<string, Record<string, any>> = {}
Object.entries(apiSheets).forEach(([sheetName, sheet]) => {
// Нормализуем имя листа: "Report" -> "L"
const normalizedName =
sheetName === 'Report'
? SHEET_NAMES.report
: sheetName === 'Calculations'
? SHEET_NAMES.calculations
: sheetName
// Интересуют только листы L и R
if (
(normalizedName === SHEET_NAMES.report ||
normalizedName === SHEET_NAMES.calculations) &&
sheet.cells &&
Object.keys(sheet.cells).length > 0
) {
userChanges[normalizedName] = sheet.cells
}
})
// 4. ОБЪЕДИНЯЕМ БАЗОВЫЕ ДАННЫЕ С ПОЛЬЗОВАТЕЛЬСКИМИ ИЗМЕНЕНИЯМИ
if (Object.keys(userChanges).length > 0) {
// Создаем объединенные данные: базовые данные файла + пользовательские изменения
const mergedData: Record<string, Record<string, any>> = {}
// Копируем базовые данные файла как основу
Object.entries(finalBaseData).forEach(([sheetName, sheetData]) => {
mergedData[sheetName] = { ...sheetData }
})
// Применяем пользовательские изменения поверх базовых данных
Object.entries(userChanges).forEach(([sheetName, sheetData]) => {
if (!mergedData[sheetName]) {
mergedData[sheetName] = {}
}
Object.entries(sheetData).forEach(([cellAddress, value]) => {
mergedData[sheetName][cellAddress] = value
})
})
multiSheetEngine.loadData(mergedData)
} else if (Object.keys(finalBaseData).length > 0) {
multiSheetEngine.loadData(finalBaseData)
}
} catch (error) {
console.error('❌ Ошибка при загрузке пользовательских изменений:', error)
// В случае ошибки используем только базовые данные
if (Object.keys(finalBaseData).length > 0) {
multiSheetEngine.loadData(finalBaseData)
}
}
setIsInitialized(true)
}, [
templateId,
templateData,
fileData,
loadSheets,
multiSheetEngine,
setTemplateData,
isInitialized,
])
// Загружаем сохраненные формулы при инициализации
// Ждем загрузки данных из React Query перед инициализацией
useEffect(() => {
// Инициализируем только когда все данные загружены или их нет вообще
const canInitialize =
!isLoadingLatestFile && !isLoadingFileData && templateId && !isInitialized
if (canInitialize) {
initializeData()
}
}, [
initializeData,
isLoadingLatestFile,
isLoadingFileData,
templateId,
isInitialized,
])
return (
<div className="flex h-full flex-col bg-background">
<FormulaBar
selectedCell={selectedCell}
formulaBarValue={formulaBarValue}
isCalculating={multiSheetEngine.isCalculating}
isEditing={isEditing}
onValueChange={handleFormulaBarChange}
onFocus={handleFormulaBarFocus}
onBlur={handleFormulaBarBlur}
onKeyDown={handleFormulaBarKeyDown}
formulaBarRef={el => {
formulaBarRef.current = el
}}
isSaving={isSaving}
isLoading={isLoading}
hasUnsavedChanges={hasUnsavedChanges}
lastSaveError={lastSaveError}
onManualSave={manualSave}
onClearError={clearError}
/>
<div
className="flex min-w-0 flex-1 gap-1 overflow-hidden"
style={{ transition: 'none' }}
>
{SHEET_TYPES.map((sheetType, index) => (
<React.Fragment key={sheetType}>
<Spreadsheet
sheetType={sheetType}
columnWidths={columnWidths[sheetType]}
spreadsheetWidths={spreadsheetWidths}
getCachedCellData={getCachedCellData}
selectedCell={selectedCell}
multiSelection={multiSelection}
activeSheet={activeSheet}
isEditing={isEditing}
editingValue={editingValue}
highlightedCells={highlightedCells}
handleCellClick={handleCellClick}
handleCellDoubleClick={handleCellDoubleClick}
handleCellChange={handleCellChange}
handleCellReferenceClick={handleCellReferenceClick}
handleCellMouseDown={handleCellMouseDown}
handleCellMouseEnter={handleCellMouseEnter}
handleCellMouseUp={handleCellMouseUp}
isInEditMode={isEditing}
stopEditing={stopEditing}
setActiveSheet={setActiveSheet}
activeCellInputRef={activeCellInputRef}
containerRefs={containerRefs}
handleKeyDown={handleKeyDown}
headerRefs={headerRefs}
rowHeaderRefs={rowHeaderRefs}
gridRefs={gridRefs}
onGridScroll={onGridScroll}
handleColumnResize={handleColumnResize}
mergedCells={mergedCells}
/>
{index === 0 && (
<div
ref={separatorRef}
className="flex w-2 cursor-col-resize items-center justify-center bg-border transition-colors hover:bg-primary/20"
style={{ userSelect: 'none' }}
>
<div className="h-4 w-0.5 rounded-full bg-muted-foreground/50" />
</div>
)}
</React.Fragment>
))}
</div>
</div>
)
}
export default DualSpreadsheet