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

@@ -0,0 +1,126 @@
import {
ApiSheet,
createSheet,
getSheetsByTemplate,
patchSheet,
} from '../../service/sheetApiService'
import { SaveAdapter } from '../useAutoSave'
// Утилита для извлечения UUID из templateId
const extractUUID = (id: string): string => {
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
}
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
}
/**
* Адаптер для сохранения через API
*/
export function createApiSaveAdapter(templateId: string): SaveAdapter {
let sheetsCache: Record<string, ApiSheet> = {}
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)) {
throw new Error('Invalid template ID format')
}
return {
async save(data: Record<string, Record<string, any>>) {
const sheetNames = Object.keys(data)
if (sheetNames.length === 0) return
const savePromises = sheetNames.map(async sheetName => {
const sheetCells = data[sheetName]
const existingSheet = sheetsCache[sheetName]
if (!sheetCells || Object.keys(sheetCells).length === 0) {
return
}
if (existingSheet) {
// Обновляем существующий лист
const mergedCells = {
...existingSheet.cells,
...sheetCells,
}
const isChanged = Object.keys(sheetCells).some(
key => existingSheet.cells[key] !== sheetCells[key]
)
if (!isChanged) return
await patchSheet(existingSheet.id, {
cells: mergedCells,
})
sheetsCache[sheetName] = {
...existingSheet,
cells: mergedCells,
updated_at: new Date().toISOString(),
}
} else {
// Создаем новый лист
const newSheet = await createSheet({
name: sheetName,
cells: sheetCells,
template_id: cleanTemplateId,
})
sheetsCache[sheetName] = {
id: newSheet.id,
name: sheetName,
cells: sheetCells,
template_id: cleanTemplateId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
deleted_at: null,
}
}
})
await Promise.all(savePromises)
},
async load() {
try {
const sheets = await getSheetsByTemplate(cleanTemplateId)
const sheetsMap: Record<string, ApiSheet> = {}
const result: Record<string, Record<string, any>> = {}
sheets.forEach(sheet => {
sheetsMap[sheet.name] = sheet
if (sheet.cells && Object.keys(sheet.cells).length > 0) {
result[sheet.name] = sheet.cells
}
})
sheetsCache = sheetsMap
return result
} catch (error) {
console.error('Ошибка при загрузке листов:', error)
return {}
}
},
async clear() {
// Для API очистка может означать удаление всех листов
// Пока просто очищаем кеш
sheetsCache = {}
},
}
}

View File

@@ -0,0 +1,86 @@
import { SaveAdapter } from '../useAutoSave'
interface SavedLocalStorageData {
templateId: string
formulaData: Record<string, Record<string, any>>
timestamp: string
version: number
}
// Ключ для localStorage
const getStorageKey = (templateId: string) => {
return `formula_data_${templateId}`
}
/**
* Адаптер для сохранения в localStorage
*/
export function createLocalStorageSaveAdapter(templateId: string): SaveAdapter {
let versionCounter = 1
// Инициализируем версию из существующих данных
try {
const storageKey = getStorageKey(templateId)
const savedDataStr = localStorage.getItem(storageKey)
if (savedDataStr) {
const parsedData: SavedLocalStorageData = JSON.parse(savedDataStr)
versionCounter = parsedData.version + 1
}
} catch (error) {
console.warn('⚠️ Ошибка при загрузке данных из localStorage:', error)
}
return {
async save(data: Record<string, Record<string, any>>) {
// Имитируем задержку API для реалистичности
await new Promise(resolve =>
setTimeout(resolve, 200 + Math.random() * 300)
)
// Иногда имитируем ошибку (2% вероятность)
if (Math.random() < 0.02) {
throw new Error('Ошибка записи в хранилище')
}
const savedData: SavedLocalStorageData = {
templateId,
formulaData: data,
timestamp: new Date().toISOString(),
version: versionCounter++,
}
const storageKey = getStorageKey(templateId)
localStorage.setItem(storageKey, JSON.stringify(savedData))
},
async load() {
try {
const storageKey = getStorageKey(templateId)
const savedDataStr = localStorage.getItem(storageKey)
if (savedDataStr) {
const parsedData: SavedLocalStorageData = JSON.parse(savedDataStr)
return parsedData.formulaData
}
return {}
} catch (error) {
console.error(
'❌ Ошибка при загрузке сохраненных данных из localStorage:',
error
)
return {}
}
},
async clear() {
try {
const storageKey = getStorageKey(templateId)
localStorage.removeItem(storageKey)
versionCounter = 1
} catch (error) {
console.error('❌ Ошибка при очистке сохраненных данных:', error)
}
},
}
}

197
src/hook/useAutoSave.ts Normal file
View File

@@ -0,0 +1,197 @@
import { useCallback, useEffect, useRef, useState } from 'react'
/**
* Адаптер для различных способов сохранения
*/
export interface SaveAdapter {
save: (data: Record<string, Record<string, any>>) => Promise<void>
load?: () => Promise<Record<string, Record<string, any>>>
clear?: () => Promise<void>
}
export interface UseAutoSaveOptions {
templateId?: string
autoSaveDelay?: number
enableAutoSave?: boolean
adapter: SaveAdapter
}
/**
* Универсальный хук автосохранения
* Поддерживает различные адаптеры сохранения (API, localStorage, etc.)
*/
export const useAutoSave = (
getAllModifiedCells: () => Record<string, Record<string, any>>,
isCalculating: boolean,
revision: number,
options: UseAutoSaveOptions
) => {
const {
templateId,
autoSaveDelay = 2000,
enableAutoSave = true,
adapter,
} = options
// Состояние
const [lastSavedRevision, setLastSavedRevision] = useState(0)
const [isSaving, setIsSaving] = useState(false)
const [lastSaveError, setLastSaveError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
// Refs
const saveTimeoutRef = useRef<number | null>(null)
// Основная функция сохранения
const performSave = useCallback(
async (data: Record<string, Record<string, any>>) => {
if (Object.keys(data).length === 0) {
return
}
if (!templateId) {
console.warn('⚠️ Не указан templateId для сохранения')
return
}
setIsSaving(true)
setLastSaveError(null)
try {
await adapter.save(data)
setLastSavedRevision(revision)
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Неизвестная ошибка'
setLastSaveError(errorMessage)
} finally {
setIsSaving(false)
}
},
[templateId, adapter, revision]
)
// Автосохранение с дебаунсом
const scheduleAutoSave = useCallback(() => {
if (!enableAutoSave || isCalculating || isSaving || !templateId) {
return
}
// Очищаем предыдущий таймер
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
}
saveTimeoutRef.current = setTimeout(() => {
const currentData = getAllModifiedCells()
performSave(currentData)
}, autoSaveDelay)
}, [
enableAutoSave,
isCalculating,
isSaving,
templateId,
autoSaveDelay,
getAllModifiedCells,
performSave,
])
// Эффект для автосохранения при изменениях
useEffect(() => {
if (
enableAutoSave &&
revision > lastSavedRevision &&
!isCalculating &&
!isSaving
) {
scheduleAutoSave()
}
}, [
enableAutoSave,
revision,
lastSavedRevision,
isCalculating,
isSaving,
scheduleAutoSave,
])
// Ручное сохранение
const manualSave = useCallback(() => {
// Отменяем автосохранение если оно запланировано
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
saveTimeoutRef.current = null
}
const currentData = getAllModifiedCells()
if (Object.keys(currentData).length === 0) {
return
}
performSave(currentData)
}, [getAllModifiedCells, performSave])
// Загрузка данных
const loadData = useCallback(async () => {
if (!adapter.load) return {}
setIsLoading(true)
try {
return await adapter.load()
} catch (error) {
setLastSaveError(
error instanceof Error ? error.message : 'Ошибка загрузки'
)
return {}
} finally {
setIsLoading(false)
}
}, [adapter])
// Очистка данных
const clearData = useCallback(async () => {
if (!adapter.clear) return
try {
await adapter.clear()
setLastSavedRevision(0)
} catch (error) {
setLastSaveError(
error instanceof Error ? error.message : 'Ошибка очистки'
)
}
}, [adapter])
// Очистка таймаута при размонтировании
useEffect(() => {
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
}
}
}, [])
// Вычисляемые значения
const isDataChanged = revision > lastSavedRevision
const hasUnsavedChanges = isDataChanged && !isSaving
return {
// Состояние
isSaving,
isLoading,
isDataChanged,
hasUnsavedChanges,
lastSaveError,
lastSavedRevision,
// Методы
manualSave,
loadData,
clearData,
// Утилиты
getCurrentData: getAllModifiedCells,
clearError: () => setLastSaveError(null),
}
}

View File

@@ -0,0 +1,59 @@
import { createLocalStorageSaveAdapter } from './adapters/localStorageSaveAdapter'
import { useAutoSave } from './useAutoSave'
export interface UseFormulaAutoSaveOptions {
templateId?: string
autoSaveDelay?: number
enableAutoSave?: boolean
}
/**
* Хук для автосохранения формул в localStorage
* Теперь использует универсальный хук с localStorage адаптером
*/
export const useFormulaAutoSave = (
getAllModifiedCells: () => Record<string, Record<string, any>>,
isCalculating: boolean,
revision: number,
options: UseFormulaAutoSaveOptions = {}
) => {
const { templateId, autoSaveDelay = 2000, enableAutoSave = true } = options
if (!templateId) {
// Возвращаем пустую реализацию если нет templateId
return {
isSaving: false,
isDataChanged: false,
hasUnsavedChanges: false,
lastSaveError: null,
lastSavedRevision: 0,
savedData: null,
manualSave: () => {},
loadSavedFormulas: () => null,
clearSavedFormulas: () => {},
getCurrentData: getAllModifiedCells,
clearError: () => {},
}
}
const adapter = createLocalStorageSaveAdapter(templateId)
const autoSaveResult = useAutoSave(
getAllModifiedCells,
isCalculating,
revision,
{
templateId,
autoSaveDelay,
enableAutoSave,
adapter,
}
)
return {
...autoSaveResult,
savedData: null, // Для совместимости
loadSavedFormulas: async () => autoSaveResult.loadData(),
clearSavedFormulas: async () => autoSaveResult.clearData(),
}
}

View File

@@ -0,0 +1,60 @@
import { createApiSaveAdapter } from './adapters/apiSaveAdapter'
import { useAutoSave } from './useAutoSave'
export interface UseSheetAutoSaveOptions {
templateId?: string
autoSaveDelay?: number
enableAutoSave?: boolean
}
/**
* Хук для автосохранения листов через API
* Теперь использует универсальный хук с API адаптером
*/
export const useSheetAutoSave = (
getAllModifiedCells: () => Record<string, Record<string, any>>,
isCalculating: boolean,
revision: number,
options: UseSheetAutoSaveOptions = {}
) => {
const { templateId, autoSaveDelay = 2000, enableAutoSave = true } = options
if (!templateId) {
// Возвращаем пустую реализацию если нет templateId
return {
isSaving: false,
isLoading: false,
isDataChanged: false,
hasUnsavedChanges: false,
lastSaveError: null,
lastSavedRevision: 0,
sheets: {},
manualSave: () => {},
loadSavedSheets: () => ({}),
loadSheets: async () => ({}),
getCurrentData: getAllModifiedCells,
clearError: () => {},
}
}
const adapter = createApiSaveAdapter(templateId)
const autoSaveResult = useAutoSave(
getAllModifiedCells,
isCalculating,
revision,
{
templateId,
autoSaveDelay,
enableAutoSave,
adapter,
}
)
return {
...autoSaveResult,
sheets: {}, // Для совместимости
loadSavedSheets: async () => autoSaveResult.loadData(),
loadSheets: async () => autoSaveResult.loadData(),
}
}