AI рефакторинг !

This commit is contained in:
2025-07-21 18:19:18 +03:00
parent 5737e37386
commit 47e0c7f81c
20 changed files with 1262 additions and 392 deletions

View File

@@ -1,6 +1,6 @@
import { useMultiSheetEngine } from '@/hook/useMultiSheetEngine'
import { useSheetAutoSave } from '@/hook/useSheetAutoSave'
import { getFileData, getLatestFileForTemplate } from '@/service/fileApiSevice'
import { getFileData, getLatestFileForTemplate } from '@/service/fileApiService'
import { produce } from 'immer'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -24,25 +24,36 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
templateId,
onEngineReady,
}) => {
// Мемоизируем конфигурацию листов
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: [
{ name: SHEET_NAMES.report, rows: ROW_COUNT, cols: COL_COUNT },
{ name: SHEET_NAMES.calculations, rows: ROW_COUNT, cols: COL_COUNT },
],
sheets: sheetsConfig,
initialData: templateData || {},
})
// Мемоизируем объект для передачи родительскому компоненту
const engineAPI = useMemo(
() => ({
...multiSheetEngine,
updateRevision,
}),
[multiSheetEngine, updateRevision]
)
// Передаем методы движка родительскому компоненту
useEffect(() => {
if (onEngineReady) {
onEngineReady({
...multiSheetEngine,
updateRevision,
})
onEngineReady(engineAPI)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onEngineReady])
}, [onEngineReady, engineAPI])
const [activeSheet, setActiveSheet] = useState<SheetType>('report')
const [selectedCell, setSelectedCell] = useState<{
@@ -56,31 +67,61 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
Record<string, Record<string, any>>
>({}) // Базовые данные файла
const [columnWidths, setColumnWidths] = useState<Record<SheetType, number[]>>(
// Мемоизируем начальное состояние ширин колонок
const initialColumnWidths = useMemo(
() => ({
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 [columnWidths, setColumnWidths] =
useState<Record<SheetType, number[]>>(initialColumnWidths)
// Мемоизируем начальные refs объекты
const initialGridRefs = useMemo(
() => ({
report: null,
calculations: null,
}),
[]
)
const initialContainerRefs = useMemo(
() => ({
report: null,
calculations: null,
}),
[]
)
const initialHeaderRefs = useMemo(
() => ({
report: null,
calculations: null,
}),
[]
)
const initialRowHeaderRefs = useMemo(
() => ({
report: null,
calculations: null,
}),
[]
)
const gridRefs = useRef<Record<SheetType, any>>(initialGridRefs)
const containerRefs =
useRef<Record<SheetType, HTMLDivElement | null>>(initialContainerRefs)
const headerRefs =
useRef<Record<SheetType, HTMLDivElement | null>>(initialHeaderRefs)
const rowHeaderRefs =
useRef<Record<SheetType, HTMLDivElement | null>>(initialRowHeaderRefs)
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>>>(
@@ -88,45 +129,43 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
)
const lastRevision = useRef<number>(-1)
useEffect(() => {
// Используем baseFileData как источник базовых значений для сравнения
const reportBaseData = baseFileData?.[SHEET_NAMES.report]
// Мемоизируем обработчик обновления начальных значений
const updateInitialValues = useCallback(() => {
// Обновляем начальные значения только если есть данные
const hasBaseFileData = Object.keys(baseFileData).length > 0
const hasTemplateData = Object.keys(templateData).length > 0
if (reportBaseData && Object.keys(reportBaseData).length > 0) {
// Очищаем старые значения
initialTemplateValues.current = {}
Object.entries(reportBaseData).forEach(([cellName, value]) => {
initialTemplateValues.current[cellName] = String(value)
if (hasBaseFileData) {
Object.entries(baseFileData).forEach(([sheetName, sheetData]) => {
Object.entries(sheetData).forEach(([cellAddress, value]) => {
initialTemplateValues.current[cellAddress] = String(value || '')
})
})
console.log(
'✅ Загружено базовых значений файла для сравнения:',
'✅ Загружено значений baseFileData для сравнения (приоритет):',
Object.keys(initialTemplateValues.current).length
)
} else if (hasTemplateData) {
Object.entries(templateData).forEach(([sheetName, sheetData]) => {
Object.entries(sheetData).forEach(([cellAddress, value]) => {
initialTemplateValues.current[cellAddress] = String(value || '')
})
})
console.log(
'✅ Загружено значений templateData для сравнения (fallback):',
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: Нет базовых данных для сравнения')
}
console.log('⚠️ DualSpreadsheet: Нет базовых данных для сравнения')
}
}, [baseFileData, templateData])
useEffect(() => {
updateInitialValues()
}, [updateInitialValues])
// Мемоизируем обработчик скролла к выбранной ячейке
const scrollToSelectedCell = useCallback(() => {
if (selectedCell && activeSheet) {
const grid = gridRefs.current[activeSheet]
grid?.scrollToItem({
@@ -137,6 +176,10 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
}
}, [selectedCell, activeSheet])
useEffect(() => {
scrollToSelectedCell()
}, [scrollToSelectedCell])
const getCachedCellData = useCallback(
(sheetType: SheetType): Record<string, CachedCellData> => {
const cacheKey = `${sheetType}-${revision}-${JSON.stringify(columnWidths[sheetType])}`
@@ -522,10 +565,28 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
[]
)
const [spreadsheetWidths, setSpreadsheetWidths] = useState({
L: 0.5,
R: 0.5,
})
// Мемоизируем состояние ширин таблиц
const initialSpreadsheetWidths = useMemo(
() => ({
L: 0.5,
R: 0.5,
}),
[]
)
const [spreadsheetWidths, setSpreadsheetWidths] = useState(
initialSpreadsheetWidths
)
// Мемоизируем опции автосохранения
const autoSaveOptions = useMemo(
() => ({
templateId,
autoSaveDelay: 2000,
enableAutoSave: true,
}),
[templateId]
)
// Используем новый хук автосохранения
const {
@@ -541,11 +602,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
multiSheetEngine.getAllModifiedCells,
multiSheetEngine.isCalculating,
revision,
{
templateId,
autoSaveDelay: 2000,
enableAutoSave: true,
}
autoSaveOptions
)
// Отладка templateId
@@ -574,153 +631,138 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
const separatorRef = useDraggableDivider(handleDividerDrag)
// Загружаем сохраненные формулы при инициализации
useEffect(() => {
// Мемоизируем функцию инициализации данных
const initializeData = useCallback(async () => {
if (!templateId || isInitialized) return
// Асинхронная функция для загрузки и слияния данных
const initializeData = async () => {
let finalBaseData: Record<string, Record<string, any>> = {}
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
// 1. ЗАГРУЖАЕМ БАЗОВЫЕ ДАННЫЕ ФАЙЛА
if (templateId) {
try {
console.log('🔍 Загружаем пользовательские изменения из API...')
const apiSheets = await loadSheets()
const latestFile = await getLatestFileForTemplate(templateId)
if (latestFile) {
console.log(
'📁 Найден последний файл для шаблона:',
latestFile.filename
)
const fileData = await getFileData(latestFile.id)
// Преобразуем листы 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
// 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(
'📂 Загружены пользовательские изменения:',
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
multiSheetEngine.loadData(mergedData)
} else if (Object.keys(finalBaseData).length > 0) {
console.log(
'📋 Используем только базовые данные (нет пользовательских изменений)'
)
// В случае ошибки используем только базовые данные
if (Object.keys(finalBaseData).length > 0) {
console.log('📋 Ошибка API, используем только базовые данные')
multiSheetEngine.loadData(finalBaseData)
}
multiSheetEngine.loadData(finalBaseData)
} else {
console.log('⚠️ Нет данных для загрузки')
}
} catch (error) {
console.error('❌ Ошибка при загрузке пользовательских изменений:', error)
setIsInitialized(true)
// В случае ошибки используем только базовые данные
if (Object.keys(finalBaseData).length > 0) {
console.log('📋 Ошибка API, используем только базовые данные')
multiSheetEngine.loadData(finalBaseData)
}
}
initializeData()
setIsInitialized(true)
}, [
templateId,
templateData,
@@ -730,6 +772,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
isInitialized,
])
// Загружаем сохраненные формулы при инициализации
useEffect(() => {
initializeData()
}, [initializeData])
return (
<div className="flex h-full flex-col bg-background">
<FormulaBar