переименования папок
This commit is contained in:
797
src/component/DualSpreadsheet/DualSpreadsheet.tsx
Normal file
797
src/component/DualSpreadsheet/DualSpreadsheet.tsx
Normal file
@@ -0,0 +1,797 @@
|
||||
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
|
||||
Reference in New Issue
Block a user