798 lines
26 KiB
TypeScript
798 lines
26 KiB
TypeScript
import { useMultiSheetEngine } from '@/hook/useMultiSheetEngine'
|
||
import { useSheetAutoSave } from '@/hook/useSheetAutoSave'
|
||
import { getFileData, getLatestFileForTemplate } from '@/service/fileApiSevice'
|
||
import { produce } from 'immer'
|
||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||
|
||
// Импорты из новых модулей
|
||
import { FormulaBar, Spreadsheet } from './components'
|
||
import { useDraggableDivider } from './hooks/useDraggableDivider'
|
||
import {
|
||
CachedCellData,
|
||
COL_COUNT,
|
||
DualSpreadsheetProps,
|
||
ROW_COUNT,
|
||
SHEET_NAMES,
|
||
SHEET_TYPES,
|
||
SheetType,
|
||
} from './types'
|
||
import { getColumnLabel } from './utils'
|
||
|
||
const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||
templateData = {},
|
||
mergedCells = [],
|
||
templateId,
|
||
onEngineReady,
|
||
}) => {
|
||
const { revision, updateRevision, setTemplateData, ...multiSheetEngine } =
|
||
useMultiSheetEngine({
|
||
sheets: [
|
||
{ name: SHEET_NAMES.report, rows: ROW_COUNT, cols: COL_COUNT },
|
||
{ name: SHEET_NAMES.calculations, rows: ROW_COUNT, cols: COL_COUNT },
|
||
],
|
||
initialData: templateData || {},
|
||
})
|
||
|
||
// Передаем методы движка родительскому компоненту
|
||
useEffect(() => {
|
||
if (onEngineReady) {
|
||
onEngineReady({
|
||
...multiSheetEngine,
|
||
updateRevision,
|
||
})
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [onEngineReady])
|
||
|
||
const [activeSheet, setActiveSheet] = useState<SheetType>('report')
|
||
const [selectedCell, setSelectedCell] = useState<{
|
||
row: number
|
||
col: number
|
||
} | null>(null)
|
||
const [isEditing, setIsEditing] = useState(false)
|
||
const [editingValue, setEditingValue] = useState<string>('') // Локальное значение во время редактирования
|
||
const [isInitialized, setIsInitialized] = useState(false)
|
||
const [baseFileData, setBaseFileData] = useState<
|
||
Record<string, Record<string, any>>
|
||
>({}) // Базовые данные файла
|
||
|
||
const [columnWidths, setColumnWidths] = useState<Record<SheetType, number[]>>(
|
||
() => ({
|
||
report: Array(COL_COUNT).fill(96),
|
||
calculations: Array(COL_COUNT).fill(96),
|
||
})
|
||
)
|
||
const gridRefs = useRef<Record<SheetType, any>>({
|
||
report: null,
|
||
calculations: null,
|
||
})
|
||
|
||
const containerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||
report: null,
|
||
calculations: null,
|
||
})
|
||
const activeCellInputRef = useRef<HTMLInputElement>(null)
|
||
const formulaBarRef = useRef<HTMLInputElement | null>(null)
|
||
const headerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||
report: null,
|
||
calculations: null,
|
||
})
|
||
const rowHeaderRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||
report: null,
|
||
calculations: null,
|
||
})
|
||
const initialTemplateValues = useRef<Record<string, string>>({})
|
||
|
||
const cellDataCache = useRef<Record<string, Record<string, CachedCellData>>>(
|
||
{}
|
||
)
|
||
const lastRevision = useRef<number>(-1)
|
||
|
||
useEffect(() => {
|
||
// Используем baseFileData как источник базовых значений для сравнения
|
||
const reportBaseData = baseFileData?.[SHEET_NAMES.report]
|
||
|
||
if (reportBaseData && Object.keys(reportBaseData).length > 0) {
|
||
// Очищаем старые значения
|
||
initialTemplateValues.current = {}
|
||
|
||
Object.entries(reportBaseData).forEach(([cellName, value]) => {
|
||
initialTemplateValues.current[cellName] = String(value)
|
||
})
|
||
|
||
console.log(
|
||
'✅ Загружено базовых значений файла для сравнения:',
|
||
Object.keys(initialTemplateValues.current).length
|
||
)
|
||
} else {
|
||
// Fallback: используем templateData, если baseFileData еще не загружен
|
||
const reportTemplate = templateData?.[SHEET_NAMES.report]
|
||
|
||
if (reportTemplate && Object.keys(reportTemplate).length > 0) {
|
||
// Очищаем старые значения
|
||
initialTemplateValues.current = {}
|
||
|
||
Object.entries(reportTemplate).forEach(([cellName, value]) => {
|
||
initialTemplateValues.current[cellName] = String(value)
|
||
})
|
||
|
||
console.log(
|
||
'✅ Загружено значений templateData для сравнения (fallback):',
|
||
Object.keys(initialTemplateValues.current).length
|
||
)
|
||
} else {
|
||
console.log('⚠️ DualSpreadsheet: Нет базовых данных для сравнения')
|
||
}
|
||
}
|
||
}, [baseFileData, templateData])
|
||
|
||
useEffect(() => {
|
||
if (selectedCell && activeSheet) {
|
||
const grid = gridRefs.current[activeSheet]
|
||
grid?.scrollToItem({
|
||
rowIndex: selectedCell.row,
|
||
columnIndex: selectedCell.col,
|
||
align: 'auto',
|
||
})
|
||
}
|
||
}, [selectedCell, activeSheet])
|
||
|
||
const getCachedCellData = useCallback(
|
||
(sheetType: SheetType): Record<string, CachedCellData> => {
|
||
const cacheKey = `${sheetType}-${revision}-${JSON.stringify(columnWidths[sheetType])}`
|
||
|
||
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] ?? '' // Базовое значение из файла
|
||
|
||
// Отладка только для измененных ячеек
|
||
if (
|
||
sheetType === 'report' &&
|
||
baseFileValue &&
|
||
rawValue !== baseFileValue
|
||
) {
|
||
console.log(`🔧 Измененная ячейка ${cellName}:`, {
|
||
baseFileValue,
|
||
rawValue,
|
||
isModified: true,
|
||
})
|
||
}
|
||
|
||
// Нормализуем значения для сравнения
|
||
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
|
||
|
||
// Отладка только для измененных ячеек
|
||
if (sheetType === 'report' && isModified) {
|
||
console.log(`🔍 Измененная ячейка ${cellName}:`, {
|
||
baseFileValue,
|
||
rawValue,
|
||
isModified,
|
||
})
|
||
}
|
||
|
||
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 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])
|
||
|
||
const startEditing = useCallback(() => {
|
||
if (!selectedCell) return
|
||
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]
|
||
const cellName =
|
||
getColumnLabel(selectedCell.col) + (selectedCell.row + 1)
|
||
const oldValue = multiSheetEngine.getCellRawValue(
|
||
sheetName,
|
||
selectedCell.row,
|
||
selectedCell.col
|
||
)
|
||
const baseFileValue = initialTemplateValues.current[cellName] ?? '' // Базовое значение из файла
|
||
|
||
console.log('💾 Сохранение ячейки:', {
|
||
cellName,
|
||
oldValue,
|
||
newValue: editingValue,
|
||
baseFileValue,
|
||
isChanged: oldValue !== editingValue,
|
||
isModifiedFromBaseFile: baseFileValue !== editingValue,
|
||
})
|
||
|
||
multiSheetEngine.setCellValueWithoutRecalc(
|
||
sheetName,
|
||
selectedCell.row,
|
||
selectedCell.col,
|
||
editingValue
|
||
)
|
||
updateRevision()
|
||
multiSheetEngine.debouncedRecalc()
|
||
}
|
||
setIsEditing(false)
|
||
setEditingValue('')
|
||
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
||
},
|
||
[activeSheet, selectedCell, editingValue, multiSheetEngine, updateRevision]
|
||
)
|
||
|
||
const handleCellChange = useCallback(
|
||
(newValue: string) => {
|
||
if (isEditing) {
|
||
setEditingValue(newValue)
|
||
}
|
||
},
|
||
[isEditing]
|
||
)
|
||
|
||
const handleClearCell = useCallback(() => {
|
||
if (!selectedCell) return
|
||
const sheetName = SHEET_NAMES[activeSheet]
|
||
multiSheetEngine.setCellValueWithoutRecalc(
|
||
sheetName,
|
||
selectedCell.row,
|
||
selectedCell.col,
|
||
''
|
||
)
|
||
updateRevision()
|
||
multiSheetEngine.debouncedRecalc()
|
||
}, [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 })
|
||
},
|
||
[selectedCell, isEditing, stopEditing]
|
||
)
|
||
|
||
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: () => startEditing(),
|
||
F2: () => startEditing(),
|
||
Delete: handleClearCell,
|
||
Backspace: handleClearCell,
|
||
}
|
||
|
||
if (keyHandlers[e.key]) {
|
||
e.preventDefault()
|
||
keyHandlers[e.key]()
|
||
} else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
|
||
e.preventDefault()
|
||
setEditingValue(e.key)
|
||
setIsEditing(true)
|
||
}
|
||
},
|
||
[selectedCell, isEditing, moveSelection, startEditing, handleClearCell]
|
||
)
|
||
|
||
const handleCellClick = useCallback(
|
||
(row: number, col: number) => {
|
||
if (isEditing) {
|
||
stopEditing(true)
|
||
}
|
||
setSelectedCell({ row, col })
|
||
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
||
},
|
||
[isEditing, stopEditing, activeSheet]
|
||
)
|
||
|
||
const handleCellDoubleClick = useCallback(
|
||
(row: number, col: number) => {
|
||
setSelectedCell({ row, col })
|
||
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)
|
||
}
|
||
},
|
||
[selectedCell, isEditing]
|
||
)
|
||
|
||
const handleFormulaBarFocus = useCallback(() => {
|
||
if (selectedCell && !isEditing) {
|
||
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 {
|
||
isSaving,
|
||
isLoading,
|
||
hasUnsavedChanges,
|
||
lastSaveError,
|
||
manualSave,
|
||
clearError,
|
||
loadSavedSheets,
|
||
loadSheets, // Добавляем асинхронную функцию загрузки
|
||
} = useSheetAutoSave(
|
||
multiSheetEngine.getAllModifiedCells,
|
||
multiSheetEngine.isCalculating,
|
||
revision,
|
||
{
|
||
templateId,
|
||
autoSaveDelay: 2000,
|
||
enableAutoSave: true,
|
||
}
|
||
)
|
||
|
||
// Отладка templateId
|
||
useEffect(() => {
|
||
console.log('🆔 Текущий templateId в DualSpreadsheet:', {
|
||
templateId,
|
||
type: typeof templateId,
|
||
isValid: templateId
|
||
? /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
||
templateId
|
||
)
|
||
: false,
|
||
})
|
||
}, [templateId])
|
||
|
||
// Используем хук для перетаскивания разделителя
|
||
const handleDividerDrag = useCallback((newLeftPercentage: number) => {
|
||
// Используем requestAnimationFrame для более плавного обновления
|
||
requestAnimationFrame(() => {
|
||
setSpreadsheetWidths({
|
||
L: newLeftPercentage,
|
||
R: 1 - newLeftPercentage,
|
||
})
|
||
})
|
||
}, [])
|
||
|
||
const separatorRef = useDraggableDivider(handleDividerDrag)
|
||
|
||
// Загружаем сохраненные формулы при инициализации
|
||
useEffect(() => {
|
||
if (!templateId || isInitialized) return
|
||
|
||
// Асинхронная функция для загрузки и слияния данных
|
||
const initializeData = async () => {
|
||
let finalBaseData: Record<string, Record<string, any>> = {}
|
||
|
||
// 1. ЗАГРУЖАЕМ БАЗОВЫЕ ДАННЫЕ ФАЙЛА
|
||
if (templateId) {
|
||
try {
|
||
console.log('🔍 Ищем файлы для templateId:', templateId)
|
||
const latestFile = await getLatestFileForTemplate(templateId)
|
||
|
||
if (latestFile) {
|
||
console.log(
|
||
'📄 Найден файл:',
|
||
latestFile.name,
|
||
'ID:',
|
||
latestFile.id
|
||
)
|
||
const fileData = await getFileData(latestFile.id)
|
||
|
||
// TODO: Пока getFileData возвращает пустые данные, используем templateData
|
||
// Когда API будет готов, здесь будут реальные данные файла
|
||
if (Object.keys(fileData.data).length > 0) {
|
||
finalBaseData = { [SHEET_NAMES.report]: fileData.data }
|
||
setBaseFileData(finalBaseData)
|
||
// Сохраняем baseline, чтобы в L попадали только пользовательские изменения
|
||
setTemplateData(finalBaseData)
|
||
console.log('📄 Загружены базовые данные файла:', finalBaseData)
|
||
} else {
|
||
console.log(
|
||
'⚠️ getFileData вернул пустые данные, используем templateData'
|
||
)
|
||
}
|
||
} else {
|
||
console.log('⚠️ Файл не найден для templateId:', templateId)
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Ошибка при загрузке данных файла:', error)
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
console.log(
|
||
'📄 Используем templateData как базовые данные:',
|
||
finalBaseData
|
||
)
|
||
setTemplateData(templateData)
|
||
}
|
||
|
||
// 3. ЗАГРУЖАЕМ ПОЛЬЗОВАТЕЛЬСКИЕ ИЗМЕНЕНИЯ ИЗ API
|
||
try {
|
||
console.log('🔍 Загружаем пользовательские изменения из API...')
|
||
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
|
||
}
|
||
})
|
||
|
||
console.log(
|
||
'📂 Загружены пользовательские изменения:',
|
||
Object.keys(userChanges)
|
||
)
|
||
|
||
// 4. ОБЪЕДИНЯЕМ БАЗОВЫЕ ДАННЫЕ С ПОЛЬЗОВАТЕЛЬСКИМИ ИЗМЕНЕНИЯМИ
|
||
if (Object.keys(userChanges).length > 0) {
|
||
console.log(
|
||
'🔄 Объединяем базовые данные с пользовательскими изменениями'
|
||
)
|
||
|
||
// Создаем объединенные данные: базовые данные файла + пользовательские изменения
|
||
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
|
||
})
|
||
})
|
||
|
||
console.log(
|
||
'📊 Загружаем объединенные данные (базовые + пользовательские)'
|
||
)
|
||
multiSheetEngine.loadData(mergedData)
|
||
|
||
// Обновляем baseline, включая уже сохранённые изменения пользователя,
|
||
// чтобы их не сохранять повторно
|
||
} else if (Object.keys(finalBaseData).length > 0) {
|
||
console.log(
|
||
'📋 Используем только базовые данные (нет пользовательских изменений)'
|
||
)
|
||
multiSheetEngine.loadData(finalBaseData)
|
||
} else {
|
||
console.log('⚠️ Нет данных для загрузки')
|
||
}
|
||
} catch (error) {
|
||
console.error(
|
||
'❌ Ошибка при загрузке пользовательских изменений:',
|
||
error
|
||
)
|
||
|
||
// В случае ошибки используем только базовые данные
|
||
if (Object.keys(finalBaseData).length > 0) {
|
||
console.log('📋 Ошибка API, используем только базовые данные')
|
||
multiSheetEngine.loadData(finalBaseData)
|
||
}
|
||
}
|
||
|
||
setIsInitialized(true)
|
||
}
|
||
|
||
initializeData()
|
||
}, [
|
||
templateId,
|
||
templateData,
|
||
loadSheets,
|
||
multiSheetEngine,
|
||
setTemplateData,
|
||
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) => (
|
||
<Spreadsheet
|
||
key={sheetType}
|
||
sheetType={sheetType}
|
||
columnWidths={columnWidths[sheetType]}
|
||
spreadsheetWidths={spreadsheetWidths}
|
||
getCachedCellData={getCachedCellData}
|
||
selectedCell={selectedCell}
|
||
activeSheet={activeSheet}
|
||
isEditing={isEditing}
|
||
editingValue={editingValue}
|
||
handleCellClick={handleCellClick}
|
||
handleCellDoubleClick={handleCellDoubleClick}
|
||
handleCellChange={handleCellChange}
|
||
stopEditing={stopEditing}
|
||
setActiveSheet={setActiveSheet}
|
||
activeCellInputRef={activeCellInputRef}
|
||
containerRefs={containerRefs}
|
||
handleKeyDown={handleKeyDown}
|
||
headerRefs={headerRefs}
|
||
rowHeaderRefs={rowHeaderRefs}
|
||
gridRefs={gridRefs}
|
||
onGridScroll={onGridScroll}
|
||
handleColumnResize={handleColumnResize}
|
||
mergedCells={mergedCells}
|
||
/>
|
||
))}
|
||
<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>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default DualSpreadsheet
|