переименования папок

This commit is contained in:
2025-07-21 15:02:10 +03:00
parent d0c79bd334
commit 84f0c9c6aa
72 changed files with 245 additions and 249 deletions

View File

@@ -0,0 +1,313 @@
import { useCallback, useEffect, useRef, useState } from 'react'
export interface SavedFormulaData {
templateId?: string
formulaData: Record<string, Record<string, any>>
timestamp: string
version: number
}
export interface UseFormulaAutoSaveOptions {
templateId?: string
autoSaveDelay?: number // задержка автосохранения в мс
enableAutoSave?: boolean
}
// Ключ для localStorage
const getStorageKey = (templateId: string) => {
console.log('🔑 Получен templateId:', templateId)
return `formula_data_${templateId}`
}
/**
* Хук для автосохранения формул электронных таблиц в localStorage
*
* Функции:
* - Автоматическое сохранение с дебаунсом при изменениях
* - Ручное сохранение по требованию
* - Отслеживание состояния сохранения
* - Сохранение в localStorage (заглушка для API)
*/
export const useFormulaAutoSave = (
getAllModifiedCells: () => Record<string, Record<string, any>>,
isCalculating: boolean,
revision: number,
options: UseFormulaAutoSaveOptions = {}
) => {
const { templateId, autoSaveDelay = 2000, enableAutoSave = true } = options
// Состояние
const [lastSavedRevision, setLastSavedRevision] = useState(0)
const [isSaving, setIsSaving] = useState(false)
const [lastSaveError, setLastSaveError] = useState<string | null>(null)
const [savedData, setSavedData] = useState<SavedFormulaData | null>(null)
// Refs
const saveTimeoutRef = useRef<number | null>(null)
const versionCounter = useRef(1)
// Загрузка данных из localStorage при инициализации
useEffect(() => {
if (!templateId) return
try {
const storageKey = getStorageKey(templateId)
const savedDataStr = localStorage.getItem(storageKey)
if (savedDataStr) {
const parsedData: SavedFormulaData = JSON.parse(savedDataStr)
setSavedData(parsedData)
versionCounter.current = parsedData.version + 1
console.log(
'📂 Загружены сохраненные формулы (версия:',
parsedData.version,
')'
)
}
} catch (error) {
console.warn('⚠️ Ошибка при загрузке данных из localStorage:', error)
}
}, [templateId])
// Основная функция сохранения в localStorage
const performSave = useCallback(
async (data: Record<string, Record<string, any>>) => {
if (Object.keys(data).length === 0) {
console.log('📭 Нет данных для сохранения')
return
}
if (!templateId) {
console.warn('⚠️ Не указан templateId для сохранения формул')
return
}
setIsSaving(true)
setLastSaveError(null)
try {
// Имитируем задержку API для реалистичности
await new Promise(resolve =>
setTimeout(resolve, 200 + Math.random() * 300)
)
// Иногда имитируем ошибку (2% вероятность)
if (Math.random() < 0.02) {
throw new Error('Ошибка записи в хранилище')
}
const savedData: SavedFormulaData = {
templateId,
formulaData: data,
timestamp: new Date().toISOString(),
version: versionCounter.current++,
}
// Сохраняем в localStorage
const storageKey = getStorageKey(templateId)
localStorage.setItem(storageKey, JSON.stringify(savedData))
setSavedData(savedData)
setLastSavedRevision(revision)
console.log(
'💾 Сохранено формул:',
Object.values(data).reduce(
(sum, sheet) => sum + Object.keys(sheet).length,
0
)
)
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Неизвестная ошибка'
setLastSaveError(errorMessage)
console.error(
'❌ Ошибка при сохранении формул в localStorage:',
errorMessage
)
// Не обновляем lastSavedRevision при ошибке, чтобы попытаться снова
} finally {
setIsSaving(false)
}
},
[templateId, revision]
)
// Автосохранение с дебаунсом
const scheduleAutoSave = useCallback(() => {
console.log('⏰ scheduleAutoSave вызван, проверяем условия:', {
enableAutoSave,
isCalculating,
isSaving,
templateId,
})
if (!enableAutoSave || isCalculating || isSaving || !templateId) {
console.log('❌ Автосохранение отменено по условиям')
return
}
// Очищаем предыдущий таймер
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
console.log('🔄 Очищен предыдущий таймер автосохранения')
}
console.log(`⏳ Запланировано автосохранение через ${autoSaveDelay}ms`)
saveTimeoutRef.current = setTimeout(() => {
console.log('🎯 Выполняем отложенное автосохранение')
const currentData = getAllModifiedCells()
console.log('📋 Данные для автосохранения:', currentData)
performSave(currentData)
}, autoSaveDelay)
}, [
enableAutoSave,
isCalculating,
isSaving,
templateId,
autoSaveDelay,
getAllModifiedCells,
performSave,
])
// Эффект для автосохранения при изменениях
useEffect(() => {
console.log('🔍 Проверка условий автосохранения:', {
enableAutoSave,
revision,
lastSavedRevision,
isCalculating,
isSaving,
templateId,
shouldSave:
enableAutoSave &&
revision > lastSavedRevision &&
!isCalculating &&
!isSaving,
})
if (
enableAutoSave &&
revision > lastSavedRevision &&
!isCalculating &&
!isSaving
) {
console.log('✅ Планируем автосохранение...')
scheduleAutoSave()
} else {
console.log('⏸️ Автосохранение пропущено по условиям')
}
}, [
enableAutoSave,
revision,
lastSavedRevision,
isCalculating,
isSaving,
scheduleAutoSave,
])
// Ручное сохранение
const manualSave = useCallback(() => {
console.log('🖱️ Ручное сохранение вызвано')
// Отменяем автосохранение если оно запланировано
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
saveTimeoutRef.current = null
console.log('⏹️ Отменено запланированное автосохранение')
}
const currentData = getAllModifiedCells()
console.log('📋 Данные для сохранения:', currentData)
if (Object.keys(currentData).length === 0) {
console.log('📭 Нет измененных данных для сохранения')
return
}
performSave(currentData)
}, [getAllModifiedCells, performSave])
// Функция загрузки сохраненных формул из localStorage
const loadSavedFormulas = useCallback((): Record<
string,
Record<string, any>
> | null => {
if (!templateId) return null
try {
const storageKey = getStorageKey(templateId)
const savedDataStr = localStorage.getItem(storageKey)
if (savedDataStr) {
const parsedData: SavedFormulaData = JSON.parse(savedDataStr)
console.log('📂 Формулы загружены из localStorage:', {
templateId,
version: parsedData.version,
timestamp: parsedData.timestamp,
})
return parsedData.formulaData
}
return null
} catch (error) {
console.error(
'❌ Ошибка при загрузке сохраненных формул из localStorage:',
error
)
return null
}
}, [templateId])
// Функция очистки сохраненных данных
const clearSavedFormulas = useCallback(() => {
if (!templateId) return
try {
const storageKey = getStorageKey(templateId)
localStorage.removeItem(storageKey)
setSavedData(null)
setLastSavedRevision(0)
versionCounter.current = 1
console.log('🗑️ Сохраненные формулы очищены:', { templateId })
} catch (error) {
console.error('❌ Ошибка при очистке сохраненных формул:', error)
}
}, [templateId])
// Очистка таймаута при размонтировании
useEffect(() => {
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
}
}
}, [])
// Вычисляемые значения
const isDataChanged = revision > lastSavedRevision
const hasUnsavedChanges = isDataChanged && !isSaving
return {
// Состояние
isSaving,
isDataChanged,
hasUnsavedChanges,
lastSaveError,
lastSavedRevision,
savedData,
// Методы
manualSave,
loadSavedFormulas,
clearSavedFormulas,
// Утилиты
getCurrentData: getAllModifiedCells,
clearError: () => setLastSaveError(null),
}
}

View File

@@ -0,0 +1,346 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import {
CellValue,
Workbook,
} from '../lib/spreadsheet-engine/spreadsheet-engine'
interface SheetConfig {
name: string
rows: number
cols: number
}
interface UseMultiSheetEngineProps {
sheets: SheetConfig[]
initialData?: Record<string, Record<string, any>>
}
export function useMultiSheetEngine({
sheets,
initialData = {},
}: UseMultiSheetEngineProps) {
const workbookRef = useRef<Workbook | null>(null)
const [isCalculating, setIsCalculating] = useState(false)
const [revision, setRevision] = useState(0) // Новый счетчик для обновлений
const debouncedRecalcTimeoutRef = useRef<number | null>(null)
const isRecalculatingRef = useRef(false)
// Храним изначальные данные шаблона для сравнения
const templateDataRef = useRef<Record<string, Record<string, any>>>({})
// Безопасная функция для вычислений
const safeRecalc = useCallback(() => {
if (!workbookRef.current || isRecalculatingRef.current) return
isRecalculatingRef.current = true
setIsCalculating(true)
try {
workbookRef.current.recalc()
} catch (error) {
console.warn('Ошибка при вычислении:', error)
} finally {
// Уменьшаем задержку для быстрого отклика
setTimeout(() => {
setIsCalculating(false)
isRecalculatingRef.current = false
// Увеличиваем ревизию, чтобы сообщить компонентам о необходимости обновления
setRevision(r => r + 1)
}, 50) // Уменьшено с 150ms до 50ms
}
}, [])
// Инициализация workbook и листов
useEffect(() => {
if (!workbookRef.current) {
workbookRef.current = new Workbook()
// Создаем листы
sheets.forEach(sheetConfig => {
workbookRef.current!.addSheet(sheetConfig.name)
})
// Увеличиваем ревизию для начальной отрисовки
setRevision(r => r + 1)
}
}, [sheets])
// Отдельный useEffect для перезагрузки данных при изменении initialData
useEffect(() => {
if (!workbookRef.current) return
if (Object.keys(initialData).length > 0) {
// Очищаем только те листы, для которых есть новые данные
for (const sheetName of Object.keys(initialData)) {
const sheet = workbookRef.current.getSheet(sheetName)
if (sheet) {
sheet.cells.clear()
}
}
workbookRef.current.fromJSON(initialData)
safeRecalc()
} else {
// Если данных нет, просто обновляем ревизию
setRevision(r => r + 1)
}
}, [initialData, safeRecalc])
// Синхронизация всех листов из движка
// const syncAllSheetsFromEngine = useCallback(() => {
// if (!workbookRef.current || isRecalculatingRef.current) return
// const newSheetCells: Record<string, CellData[][]> = {}
// sheets.forEach((sheetConfig) => {
// const sheet = workbookRef.current!.getSheet(sheetConfig.name)
// if (!sheet) return
// const cells = Array(sheetConfig.rows)
// .fill(null)
// .map(() =>
// Array(sheetConfig.cols)
// .fill(null)
// .map(() => ({ value: '', isSelected: false })),
// )
// // Получаем все ячейки из движка
// for (let row = 0; row < sheetConfig.rows; row++) {
// for (let col = 0; col < sheetConfig.cols; col++) {
// const cellName = columnToLabel(col) + (row + 1)
// const cell = sheet.getCell(cellName)
// if (cell.raw !== null && cell.raw !== undefined) {
// cells[row][col] = String(cell.raw)
// }
// }
// }
// newSheetCells[sheetConfig.name] = cells
// })
// // setSheetCells(newSheetCells) // Удалено, так как sheetCells убрано
// }, [sheets])
// Отложенный пересчет с дебаунсом
const debouncedRecalc = useCallback(() => {
if (!workbookRef.current || isRecalculatingRef.current) return
// Очищаем предыдущий отложенный пересчет
if (debouncedRecalcTimeoutRef.current) {
clearTimeout(debouncedRecalcTimeoutRef.current)
}
// Уменьшаем задержку для быстрого отклика
debouncedRecalcTimeoutRef.current = setTimeout(() => {
safeRecalc()
}, 100) // Уменьшено с 300ms до 100ms
}, [safeRecalc])
// Функция для немедленного обновления интерфейса без пересчета
const updateRevision = useCallback(() => {
setRevision(r => r + 1)
}, [])
// Установка значения ячейки без немедленного пересчета
const setCellValueWithoutRecalc = useCallback(
(sheetName: string, row: number, col: number, value: string) => {
if (!workbookRef.current || isRecalculatingRef.current) return
const cellName = columnToLabel(col) + (row + 1)
const qualifiedName = `${sheetName}!${cellName}`
// Устанавливаем значение в движке без пересчета
workbookRef.current.setCell(qualifiedName, value)
},
[]
)
// Установка значения ячейки с немедленным пересчетом
const setCellValue = useCallback(
(sheetName: string, row: number, col: number, value: string) => {
if (!workbookRef.current || isRecalculatingRef.current) return
const cellName = columnToLabel(col) + (row + 1)
const qualifiedName = `${sheetName}!${cellName}`
// Устанавливаем значение в движке
workbookRef.current.setCell(qualifiedName, value)
// Запускаем безопасный пересчет
safeRecalc()
},
[safeRecalc]
)
// Получение значения ячейки (вычисленное)
const getCellValue = useCallback(
(sheetName: string, row: number, col: number): CellValue => {
if (!workbookRef.current) return ''
const cellName = columnToLabel(col) + (row + 1)
const qualifiedName = `${sheetName}!${cellName}`
return workbookRef.current.getValue(qualifiedName)
},
[]
)
// Получение сырого значения ячейки (формула)
const getCellRawValue = useCallback(
(sheetName: string, row: number, col: number): string => {
if (!workbookRef.current) return ''
const sheet = workbookRef.current.getSheet(sheetName)
if (!sheet) return ''
const cellName = columnToLabel(col) + (row + 1)
const cell = sheet.getCell(cellName)
return String(cell.raw || '')
},
[]
)
// Получение всех измененных ячеек для листа
const getModifiedCells = useCallback(
(sheetName: string): Record<string, CellValue> => {
if (!workbookRef.current) return {}
const sheet = workbookRef.current.getSheet(sheetName)
if (!sheet) return {}
return sheet.toJSON()
},
[]
)
// Установка изначальных данных шаблона
const setTemplateData = useCallback(
(data: Record<string, Record<string, any>>) => {
templateDataRef.current = JSON.parse(JSON.stringify(data)) // Глубокая копия
console.log('📋 Установлены данные шаблона:', templateDataRef.current)
},
[]
)
// Получение всех измененных ячеек для всех листов (только пользовательские изменения)
const getAllModifiedCells = useCallback((): Record<
string,
Record<string, CellValue>
> => {
if (!workbookRef.current) return {}
const allData = workbookRef.current.toJSON()
const modifiedData: Record<string, Record<string, CellValue>> = {}
// Список всех листов (из движка или baseline)
const sheetNamesSet = new Set([
...Object.keys(allData),
...Object.keys(templateDataRef.current),
])
sheetNamesSet.forEach(sheetName => {
const sheetData = allData[sheetName] || {}
const templateSheet = templateDataRef.current[sheetName] || {}
const cellAddresses = new Set([
...Object.keys(sheetData),
...Object.keys(templateSheet),
])
const modifiedSheet: Record<string, CellValue> = {}
cellAddresses.forEach(cellAddress => {
const currentVal = sheetData[cellAddress] ?? ''
const templateVal = templateSheet[cellAddress] ?? ''
const normalizeValue = (val: any): string => {
if (val === null || val === undefined) return ''
const str = String(val).trim()
if (str === '0' || str === '') return ''
return str
}
const normalizedCurrent = normalizeValue(currentVal)
const normalizedTemplate = normalizeValue(templateVal)
if (normalizedCurrent !== normalizedTemplate) {
// Если текущего значения нет (ячейка очищена), отправляем ''
modifiedSheet[cellAddress] = currentVal
console.log(
`🔧 Пользовательское изменение ${sheetName}:${cellAddress}:`,
{
template: normalizedTemplate,
current: normalizedCurrent,
value: currentVal,
}
)
}
})
if (Object.keys(modifiedSheet).length > 0) {
modifiedData[sheetName] = modifiedSheet
}
})
return modifiedData
}, [])
// Очистка всех данных
const clearAllData = useCallback(() => {
if (!workbookRef.current) return
workbookRef.current.clear()
setRevision(r => r + 1)
}, [])
// Загрузка данных из JSON
const loadData = useCallback(
(data: Record<string, Record<string, any>>) => {
if (!workbookRef.current) return
workbookRef.current.fromJSON(data)
safeRecalc()
},
[safeRecalc]
)
// Пересчет всех формул
const recalculate = useCallback(() => {
safeRecalc()
}, [safeRecalc])
// Очистка таймаутов при размонтировании
useEffect(() => {
return () => {
if (debouncedRecalcTimeoutRef.current) {
clearTimeout(debouncedRecalcTimeoutRef.current)
}
isRecalculatingRef.current = false
}
}, [])
return {
// sheetCells убран
setCellValue,
setCellValueWithoutRecalc,
debouncedRecalc,
updateRevision, // Добавляем новую функцию
getCellValue,
getCellRawValue,
getModifiedCells,
getAllModifiedCells,
setTemplateData, // Экспортируем функцию установки данных шаблона
clearAllData,
loadData,
recalculate,
isCalculating,
workbook: workbookRef.current,
revision, // Экспортируем ревизию
}
}
// Вспомогательная функция для конвертации индекса колонки в букву
function columnToLabel(index: number): string {
return String.fromCharCode(65 + index)
}

View File

@@ -0,0 +1,387 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import {
ApiSheet,
createSheet,
getSheetsByTemplate,
patchSheet,
} from '../service/sheetApiService'
export interface UseSheetAutoSaveOptions {
templateId?: string
autoSaveDelay?: number // задержка автосохранения в мс
enableAutoSave?: boolean
}
/**
* Хук для автосохранения листов через API
*
* Функции:
* - Автоматическое сохранение с дебаунсом при изменениях
* - Ручное сохранение по требованию
* - Создание и обновление листов через API
* - Загрузка существующих листов
*/
// Утилита для извлечения UUID из templateId
const extractUUID = (id: string): string => {
// Если это уже чистый UUID, возвращаем как есть
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
if (uuidRegex.test(id)) {
return id
}
// Если есть префикс (например "template-"), извлекаем UUID
const match = id.match(
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
)
if (match) {
return match[0]
}
return id // Возвращаем оригинал если не найден UUID
}
export const useSheetAutoSave = (
getAllModifiedCells: () => Record<string, Record<string, any>>,
isCalculating: boolean,
revision: number,
options: UseSheetAutoSaveOptions = {}
) => {
const { templateId, autoSaveDelay = 2000, enableAutoSave = true } = options
// Состояние
const [lastSavedRevision, setLastSavedRevision] = useState(0)
const [isSaving, setIsSaving] = useState(false)
const [lastSaveError, setLastSaveError] = useState<string | null>(null)
const [sheets, setSheets] = useState<Record<string, ApiSheet>>({})
const [isLoading, setIsLoading] = useState(false)
// Refs
const saveTimeoutRef = useRef<number | null>(null)
// Загрузка листов для шаблона при инициализации
const loadSheets = useCallback(async () => {
if (!templateId) return {}
const cleanTemplateId = extractUUID(templateId)
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
if (!uuidRegex.test(cleanTemplateId)) {
console.error('❌ Не удалось извлечь валидный UUID для загрузки:', {
original: templateId,
extracted: cleanTemplateId,
})
setLastSaveError('Неверный ID шаблона')
return {}
}
setIsLoading(true)
try {
const response = await getSheetsByTemplate(cleanTemplateId)
const sheetsMap = response.sheets.reduce(
(acc, sheet) => {
const normalizedName = sheet.name === 'Report' ? 'L' : sheet.name
acc[normalizedName] = { ...sheet, name: normalizedName }
return acc
},
{} as Record<string, ApiSheet>
)
setSheets(sheetsMap)
console.log('📂 Загружены листы для шаблона:', Object.keys(sheetsMap))
return sheetsMap
} catch (error) {
console.error('❌ Ошибка при загрузке листов:', error)
setLastSaveError('Ошибка загрузки листов')
return {}
} finally {
setIsLoading(false)
}
}, [templateId])
// Загрузка листов при изменении templateId
useEffect(() => {
if (templateId) {
loadSheets()
}
}, [templateId, loadSheets])
// Основная функция сохранения через API
const performSave = useCallback(
async (data: Record<string, Record<string, any>>) => {
// Сохраняем все листы, но пропускаем пустые
const sheetNames = Object.keys(data)
if (sheetNames.length === 0) {
console.log('📭 Нет данных для сохранения')
return
}
if (!templateId) {
console.warn('⚠️ Не указан templateId для сохранения листов')
return
}
// Извлекаем и валидируем UUID из templateId
const cleanTemplateId = extractUUID(templateId)
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
if (!uuidRegex.test(cleanTemplateId)) {
console.error('❌ Не удалось извлечь валидный UUID из templateId:', {
original: templateId,
extracted: cleanTemplateId,
})
setLastSaveError('Неверный ID шаблона')
return
}
console.log('🧹 Очищенный templateId:', {
original: templateId,
clean: cleanTemplateId,
})
setIsSaving(true)
setLastSaveError(null)
try {
const savePromises = sheetNames.map(async sheetName => {
const sheetCells = data[sheetName]
const existingSheet = sheets[sheetName]
// Проверяем что есть данные для сохранения
if (!sheetCells || Object.keys(sheetCells).length === 0) {
console.log(`⏭️ Пропускаем лист "${sheetName}" - нет данных`)
return
}
if (existingSheet) {
// Объединяем старые и новые данные, чтобы не потерять предыдущие изменения
const mergedCells = {
...existingSheet.cells,
...sheetCells,
}
// Проверяем, изменилось ли что-нибудь
const isChanged = Object.keys(sheetCells).some(
key => existingSheet.cells[key] !== sheetCells[key]
)
if (!isChanged) {
console.log(`⏭️ Лист "${sheetName}" без новых изменений`)
return
}
console.log(`🔄 Обновляем лист "${sheetName}" (patch)`)
await patchSheet(existingSheet.id, {
cells: mergedCells,
})
// Обновляем локальное состояние
setSheets(prev => ({
...prev,
[sheetName]: {
...existingSheet,
cells: mergedCells,
updated_at: new Date().toISOString(),
},
}))
} else {
// Создаем новый лист
console.log(`✨ Создаем новый лист "${sheetName}"`)
console.log('📋 Данные для создания:', {
name: sheetName,
cells: sheetCells,
template_id: cleanTemplateId,
cellsCount: Object.keys(sheetCells).length,
})
const newSheet = await createSheet({
name: sheetName,
cells: sheetCells,
template_id: cleanTemplateId,
})
// Получаем полную информацию о созданном листе
const fullSheetData: ApiSheet = {
id: newSheet.id,
name: sheetName,
cells: sheetCells,
template_id: cleanTemplateId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
deleted_at: null,
}
// Обновляем локальное состояние
setSheets(prev => ({
...prev,
[sheetName]: fullSheetData,
}))
}
})
await Promise.all(savePromises)
setLastSavedRevision(revision)
const totalCells = Object.values(data).reduce(
(sum, sheet) => sum + Object.keys(sheet).length,
0
)
console.log('💾 Сохранено ячеек:', totalCells, 'в листах:', sheetNames)
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Неизвестная ошибка'
setLastSaveError(errorMessage)
console.error('❌ Ошибка при сохранении листов:', errorMessage)
} finally {
setIsSaving(false)
}
},
[templateId, sheets, revision]
)
// Автосохранение с дебаунсом
const scheduleAutoSave = useCallback(() => {
console.log('⏰ scheduleAutoSave вызван, проверяем условия:', {
enableAutoSave,
isCalculating,
isSaving,
templateId,
})
if (!enableAutoSave || isCalculating || isSaving || !templateId) {
console.log('❌ Автосохранение отменено по условиям')
return
}
// Очищаем предыдущий таймер
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
console.log('🔄 Очищен предыдущий таймер автосохранения')
}
console.log(`⏳ Запланировано автосохранение через ${autoSaveDelay}ms`)
saveTimeoutRef.current = setTimeout(() => {
console.log('🎯 Выполняем отложенное автосохранение')
const currentData = getAllModifiedCells()
console.log('📋 Данные для автосохранения:', currentData)
performSave(currentData)
}, autoSaveDelay)
}, [
enableAutoSave,
isCalculating,
isSaving,
templateId,
autoSaveDelay,
getAllModifiedCells,
performSave,
])
// Эффект для автосохранения при изменениях
useEffect(() => {
console.log('🔍 Проверка условий автосохранения:', {
enableAutoSave,
revision,
lastSavedRevision,
isCalculating,
isSaving,
templateId,
shouldSave:
enableAutoSave &&
revision > lastSavedRevision &&
!isCalculating &&
!isSaving,
})
if (
enableAutoSave &&
revision > lastSavedRevision &&
!isCalculating &&
!isSaving
) {
console.log('✅ Планируем автосохранение...')
scheduleAutoSave()
} else {
console.log('⏸️ Автосохранение пропущено по условиям')
}
}, [
enableAutoSave,
revision,
lastSavedRevision,
isCalculating,
isSaving,
scheduleAutoSave,
])
// Ручное сохранение
const manualSave = useCallback(() => {
console.log('🖱️ Ручное сохранение вызвано')
// Отменяем автосохранение если оно запланировано
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
saveTimeoutRef.current = null
console.log('⏹️ Отменено запланированное автосохранение')
}
const currentData = getAllModifiedCells()
console.log('📋 Данные для сохранения:', currentData)
if (Object.keys(currentData).length === 0) {
console.log('📭 Нет измененных данных для сохранения')
return
}
performSave(currentData)
}, [getAllModifiedCells, performSave])
// Функция загрузки сохраненных листов
const loadSavedSheets = useCallback((): Record<
string,
Record<string, any>
> => {
const result: Record<string, Record<string, any>> = {}
Object.entries(sheets).forEach(([sheetName, sheet]) => {
if (sheet.cells && Object.keys(sheet.cells).length > 0) {
result[sheetName] = sheet.cells
}
})
console.log('📂 Загружены сохраненные листы:', Object.keys(result))
return result
}, [sheets])
// Очистка таймаута при размонтировании
useEffect(() => {
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
}
}
}, [])
// Вычисляемые значения
const isDataChanged = revision > lastSavedRevision
const hasUnsavedChanges = isDataChanged && !isSaving
return {
// Состояние
isSaving,
isLoading,
isDataChanged,
hasUnsavedChanges,
lastSaveError,
lastSavedRevision,
sheets,
// Методы
manualSave,
loadSavedSheets,
loadSheets,
// Утилиты
getCurrentData: getAllModifiedCells,
clearError: () => setLastSaveError(null),
}
}

View File

@@ -0,0 +1,335 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import {
CellUtils,
CellValue,
Workbook,
} from '../lib/spreadsheet-engine/spreadsheet-engine'
interface CellData {
value: string
isSelected: boolean
}
interface UseSpreadsheetEngineProps {
rows?: number
cols?: number
sheetName?: string
initialData?: string[][]
}
export function useSpreadsheetEngine({
rows = 50,
cols = 20,
sheetName = 'Sheet1',
initialData,
}: UseSpreadsheetEngineProps = {}) {
const workbookRef = useRef<Workbook | null>(null)
const [cells, setCells] = useState<CellData[][]>(() => {
return Array(rows)
.fill(null)
.map(() =>
Array(cols)
.fill(null)
.map(() => ({ value: '', isSelected: false }))
)
})
const [isCalculating, setIsCalculating] = useState(false)
// Инициализация workbook
useEffect(() => {
if (!workbookRef.current) {
workbookRef.current = new Workbook()
workbookRef.current.addSheet(sheetName)
// addCustomFunctions(workbookRef.current)
}
// Загружаем начальные данные если они есть
if (initialData) {
loadDataFromArray(initialData)
}
}, [sheetName, initialData])
// Конвертация данных из движка в React состояние
const syncFromEngine = useCallback(() => {
if (!workbookRef.current) return
const sheet = workbookRef.current.getSheet(sheetName)
if (!sheet) return
const newCells = Array(rows)
.fill(null)
.map(() =>
Array(cols)
.fill(null)
.map(() => ({ value: '', isSelected: false }))
)
// Получаем все ячейки из движка
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const cellName = CellUtils.createCellName(col, row + 1)
const cell = sheet.getCell(cellName)
// Показываем сырое значение (raw) в UI, а не вычисленное
if (cell.raw !== null && cell.raw !== undefined) {
newCells[row][col].value = String(cell.raw)
}
}
}
setCells(newCells)
}, [rows, cols, sheetName])
// Установка значения ячейки
const setCellValue = useCallback(
(row: number, col: number, value: string) => {
if (!workbookRef.current) return
const cellName = CellUtils.createCellName(col, row + 1)
const qualifiedName = `${sheetName}!${cellName}`
setIsCalculating(true)
try {
// Конвертируем пустую строку в null
const cellValue: CellValue = value === '' ? null : value
// Если значение начинается с =, то это формула
if (typeof cellValue === 'string' && cellValue.startsWith('=')) {
workbookRef.current.setCell(qualifiedName, cellValue)
} else {
// Пытаемся конвертировать в число, если возможно
const numValue = Number(cellValue)
if (!isNaN(numValue) && cellValue !== null && cellValue !== '') {
workbookRef.current.setCell(qualifiedName, numValue)
} else {
workbookRef.current.setCell(qualifiedName, cellValue)
}
}
// Пересчитываем все формулы
workbookRef.current.recalc()
// Синхронизируем с React состоянием
syncFromEngine()
} catch (error) {
console.error('Error setting cell value:', error)
} finally {
setIsCalculating(false)
}
},
[sheetName, syncFromEngine]
)
// Получение вычисленного значения ячейки
const getCellValue = useCallback(
(row: number, col: number): CellValue => {
if (!workbookRef.current) return null
const cellName = CellUtils.createCellName(col, row + 1)
const qualifiedName = `${sheetName}!${cellName}`
try {
return workbookRef.current.getValue(qualifiedName)
} catch (error) {
console.error('Error getting cell value:', error)
return '#ERROR!'
}
},
[sheetName]
)
// Получение сырого значения ячейки (для отображения в строке формул)
const getCellRawValue = useCallback(
(row: number, col: number): string => {
if (!workbookRef.current) return ''
const cellName = CellUtils.createCellName(col, row + 1)
const sheet = workbookRef.current.getSheet(sheetName)
if (!sheet) return ''
const cell = sheet.getCell(cellName)
return cell.raw !== null && cell.raw !== undefined ? String(cell.raw) : ''
},
[sheetName]
)
// Загрузка данных из массива
const loadDataFromArray = useCallback(
(data: string[][]) => {
if (!workbookRef.current) return
const sheet = workbookRef.current.getSheet(sheetName)
if (!sheet) return
setIsCalculating(true)
try {
// Очищаем лист
sheet.cells.clear()
// Загружаем данные
for (let row = 0; row < data.length && row < rows; row++) {
for (let col = 0; col < data[row].length && col < cols; col++) {
const value = data[row][col]
if (value !== null && value !== undefined && value !== '') {
const cellName = CellUtils.createCellName(col, row + 1)
sheet.setCell(cellName, value)
}
}
}
// Пересчитываем все формулы
workbookRef.current.recalc()
// Синхронизируем с React состоянием
syncFromEngine()
} catch (error) {
console.error('Error loading data:', error)
} finally {
setIsCalculating(false)
}
},
[rows, cols, sheetName, syncFromEngine]
)
// Экспорт данных в массив (для сохранения)
const exportToArray = useCallback((): string[][] => {
if (!workbookRef.current) return []
const sheet = workbookRef.current.getSheet(sheetName)
if (!sheet) return []
const result: string[][] = Array(rows)
.fill(null)
.map(() => Array(cols).fill(''))
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const cellName = CellUtils.createCellName(col, row + 1)
const cell = sheet.getCell(cellName)
if (cell.raw !== null && cell.raw !== undefined) {
result[row][col] = String(cell.raw)
}
}
}
return result
}, [rows, cols, sheetName])
// Экспорт в JSON для бэкенда
const exportToJSON = useCallback((): string => {
if (!workbookRef.current) return '{}'
return JSON.stringify(workbookRef.current.toJSON())
}, [])
// Импорт из JSON с бэкенда
const importFromJSON = useCallback(
(jsonData: string) => {
if (!workbookRef.current) return
setIsCalculating(true)
try {
const data = JSON.parse(jsonData)
workbookRef.current.fromJSON(data)
// addCustomFunctions(workbookRef.current)
workbookRef.current.recalc()
syncFromEngine()
} catch (error) {
console.error('Error importing from JSON:', error)
} finally {
setIsCalculating(false)
}
},
[syncFromEngine]
)
// Добавление пользовательской функции
const addFunction = useCallback(
(name: string, func: (...args: any[]) => any) => {
if (!workbookRef.current) return
workbookRef.current.addFunction(name, func)
},
[]
)
// Пересчёт всех формул
const recalculate = useCallback(() => {
if (!workbookRef.current) return
setIsCalculating(true)
try {
workbookRef.current.recalc()
syncFromEngine()
} catch (error) {
console.error('Error recalculating:', error)
} finally {
setIsCalculating(false)
}
}, [syncFromEngine])
// Получение информации о зависимостях ячейки
const getCellDependencies = useCallback(
(row: number, col: number) => {
if (!workbookRef.current) return { precedents: [], dependents: [] }
const cellName = CellUtils.createCellName(col, row + 1)
const sheet = workbookRef.current.getSheet(sheetName)
if (!sheet) return { precedents: [], dependents: [] }
const cell = sheet.getCell(cellName)
return {
precedents: Array.from(cell.precedents),
dependents: Array.from(cell.dependents),
}
},
[sheetName]
)
// Получение статистики workbook
const getWorkbookStats = useCallback(() => {
if (!workbookRef.current) return { sheets: 0, cells: 0, formulas: 0 }
let totalCells = 0
let totalFormulas = 0
for (const sheet of workbookRef.current.sheets.values()) {
totalCells += sheet.cells.size
for (const cell of sheet.cells.values()) {
if (typeof cell.raw === 'string' && cell.raw.startsWith('=')) {
totalFormulas++
}
}
}
return {
sheets: workbookRef.current.sheets.size,
cells: totalCells,
formulas: totalFormulas,
}
}, [])
return {
cells,
setCellValue,
getCellValue,
getCellRawValue,
loadDataFromArray,
exportToArray,
exportToJSON,
importFromJSON,
addFunction,
recalculate,
getCellDependencies,
getWorkbookStats,
isCalculating,
workbook: workbookRef.current,
}
}