починка багов

This commit is contained in:
2025-07-23 16:08:50 +03:00
parent 3fc352f8e9
commit 907b3e315e
24 changed files with 1249 additions and 1430 deletions

View File

@@ -0,0 +1,85 @@
import {
getFileData,
getFilesByTemplateId,
getLatestFileForTemplate,
parseExcelFile,
} from '@/service/fileApiService'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
// Ключи для React Query
export const fileKeys = {
all: ['files'] as const,
byTemplate: (templateId: string) =>
[...fileKeys.all, 'template', templateId] as const,
latest: (templateId: string) =>
[...fileKeys.byTemplate(templateId), 'latest'] as const,
data: (fileId: string) => [...fileKeys.all, 'data', fileId] as const,
}
// Хук для получения файлов по template_id с кешированием
export function useFilesByTemplate(templateId: string | undefined) {
return useQuery({
queryKey: fileKeys.byTemplate(templateId || ''),
queryFn: () => getFilesByTemplateId(templateId!),
enabled: !!templateId,
staleTime: 5 * 60 * 1000, // 5 минут
gcTime: 10 * 60 * 1000, // 10 минут кеш
})
}
// Хук для получения последнего файла для шаблона
export function useLatestFileForTemplate(templateId: string | undefined) {
return useQuery({
queryKey: fileKeys.latest(templateId || ''),
queryFn: () => getLatestFileForTemplate(templateId!),
enabled: !!templateId,
staleTime: 5 * 60 * 1000, // 5 минут
gcTime: 10 * 60 * 1000, // 10 минут кеш
})
}
// Хук для получения данных файла по ID
export function useFileData(fileId: string | undefined) {
return useQuery({
queryKey: fileKeys.data(fileId || ''),
queryFn: () => getFileData(fileId!),
enabled: !!fileId,
staleTime: 10 * 60 * 1000, // 10 минут для данных файла
gcTime: 30 * 60 * 1000, // 30 минут кеш
})
}
// Хук для загрузки Excel файла
export function useParseExcelFile() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ file, templateId }: { file: File; templateId: string }) =>
parseExcelFile(file, templateId),
onSuccess: (data, variables) => {
// Инвалидируем связанные кеши после успешной загрузки
queryClient.invalidateQueries({
queryKey: fileKeys.byTemplate(variables.templateId),
})
queryClient.invalidateQueries({
queryKey: fileKeys.latest(variables.templateId),
})
// Сохраняем данные файла в кеш
queryClient.setQueryData(fileKeys.data(data.fileId), data)
},
})
}
// Хук для предзагрузки данных файла
export function usePrefetchFileData() {
const queryClient = useQueryClient()
return (fileId: string) => {
queryClient.prefetchQuery({
queryKey: fileKeys.data(fileId),
queryFn: () => getFileData(fileId),
staleTime: 10 * 60 * 1000,
})
}
}

View File

@@ -192,10 +192,10 @@ export function useMultiSheetEngine({
}
}
// Запускаем безопасный пересчет
safeRecalc()
// Запускаем отложенный пересчет с дебаунсом (избегаем двойного пересчёта)
debouncedRecalc()
},
[safeRecalc]
[debouncedRecalc]
)
// Получение значения ячейки (вычисленное)

View File

@@ -1,11 +1,11 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useDebounce } from 'use-debounce'
import type { ApiSheet, CreateSheetInput } from '../service/sheetApiService'
import {
ApiSheet,
createSheet,
getSheetsByTemplate,
patchSheet,
} from '../service/sheetApiService'
useCreateSheet,
usePatchSheet,
useSheetsByTemplate,
} from './useSheetQueries'
export interface UseSheetAutoSaveOptions {
templateId?: string
@@ -19,133 +19,88 @@ export const useSheetAutoSave = (
revision: number,
options: UseSheetAutoSaveOptions = {}
) => {
console.log('getAllModifiedCells', getAllModifiedCells())
const memoizedGetAllModifiedCells = useCallback(getAllModifiedCells, [
getAllModifiedCells,
])
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)
// React Query хуки
const {
data: sheetsResponse,
isLoading,
error: loadError,
refetch: refetchSheets,
} = useSheetsByTemplate(templateId)
const createSheetMutation = useCreateSheet()
const patchSheetMutation = usePatchSheet()
// Мемоизируем карту листов
const sheetsMap = useMemo(() => {
if (!sheetsResponse?.sheets) return {}
return sheetsResponse.sheets.reduce<Record<string, ApiSheet>>(
(acc, sheet) => {
acc[sheet.name] = sheet
return acc
},
{}
)
}, [sheetsResponse?.sheets])
// Дебаунсинг ревизии для автосохранения
const [debouncedRevision] = useDebounce(revision, autoSaveDelay)
// Загрузка листов для шаблона при инициализации
const loadSheets = useCallback(async () => {
if (!templateId) return {}
// Состояние сохранения
const isSaving = createSheetMutation.isPending || patchSheetMutation.isPending
setIsLoading(true)
try {
const response = await getSheetsByTemplate(templateId)
const sheetsMap = response.sheets.reduce<Record<string, ApiSheet>>(
(acc, sheet) => {
const existingSheet = acc[sheet.name]
if (existingSheet) {
// Если лист уже есть, объединяем ячейки
acc[sheet.name] = {
...sheet, // берем актуальную версию листа
cells: {
...existingSheet.cells, // старые ячейки
...sheet.cells, // новые заменяют старые с тем же ключом
},
}
} else {
acc[sheet.name] = { ...sheet }
}
return acc
},
{}
)
setSheets(sheetsMap)
return sheetsMap
} catch (error) {
console.error('❌ Ошибка при загрузке листов:', error)
setLastSaveError('Ошибка загрузки листов')
return {}
} finally {
setIsLoading(false)
}
}, [templateId])
// Загрузка листов при изменении templateId
// Обновляем ошибку при ошибках загрузки или сохранения
useEffect(() => {
if (templateId) {
loadSheets()
if (loadError) {
setLastSaveError('Ошибка загрузки листов')
} else if (createSheetMutation.error) {
setLastSaveError(createSheetMutation.error.message)
} else if (patchSheetMutation.error) {
setLastSaveError(patchSheetMutation.error.message)
}
}, [templateId, loadSheets])
}, [loadError, createSheetMutation.error, patchSheetMutation.error])
// Основная функция сохранения через API
// Основная функция сохранения через React Query
const performSave = useCallback(
async (data: Record<string, Record<string, any>>) => {
if (!templateId) return
// Получаем список всех листов, которые нужно обработать:
// 1. Листы с данными (из data)
// 2. Существующие листы, которые могли быть очищены
// Получаем список всех листов, которые нужно обработать
const dataSheetNames = Object.keys(data)
const existingSheetNames = Object.keys(sheets)
const existingSheetNames = Object.keys(sheetsMap)
const allSheetNames = new Set([...dataSheetNames, ...existingSheetNames])
if (allSheetNames.size === 0) return
setIsSaving(true)
setLastSaveError(null)
try {
// Создаем массив промисов для параллельного выполнения
const savePromises = Array.from(allSheetNames).map(async sheetName => {
const sheetCells = data[sheetName] || {} // если данных нет, используем пустой объект
const existingSheet = sheets[sheetName]
const existingSheet = sheetsMap[sheetName]
if (existingSheet) {
// Всегда сохраняем, даже если ячейки пустые (очистка листа)
await patchSheet(existingSheet.id, {
cells: sheetCells,
// Обновляем существующий лист
await patchSheetMutation.mutateAsync({
sheetId: existingSheet.id,
data: { cells: sheetCells },
})
// Обновляем локальное состояние
setSheets(prev => ({
...prev,
[sheetName]: {
...existingSheet,
cells: sheetCells,
updated_at: new Date().toISOString(),
},
}))
} else {
// Создаем новый лист только если есть данные
if (Object.keys(sheetCells).length > 0) {
const newSheet = await createSheet({
const createInput: CreateSheetInput = {
name: sheetName,
cells: sheetCells,
template_id: templateId,
})
// Получаем полную информацию о созданном листе
const fullSheetData: ApiSheet = {
id: newSheet.id,
name: sheetName,
cells: sheetCells,
template_id: templateId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
deleted_at: null,
}
// Обновляем локальное состояние
setSheets(prev => ({
...prev,
[sheetName]: fullSheetData,
}))
await createSheetMutation.mutateAsync(createInput)
}
}
})
@@ -157,11 +112,9 @@ export const useSheetAutoSave = (
error instanceof Error ? error.message : 'Неизвестная ошибка'
setLastSaveError(errorMessage)
console.error('❌ Ошибка при сохранении листов:', errorMessage)
} finally {
setIsSaving(false)
}
},
[templateId, sheets, revision]
[templateId, sheetsMap, revision, createSheetMutation, patchSheetMutation]
)
// Автосохранение при изменении дебаунсированной ревизии
@@ -174,8 +127,16 @@ export const useSheetAutoSave = (
templateId
if (shouldAutoSave) {
console.log('✅ Выполняем автосохранение для ревизии:', debouncedRevision)
performSave(memoizedGetAllModifiedCells())
if (process.env.NODE_ENV !== 'production') {
console.log(
'✅ Выполняем автосохранение для ревизии:',
debouncedRevision
)
}
const diff = getAllModifiedCells()
if (Object.keys(diff).length > 0) {
performSave(diff)
}
}
}, [
enableAutoSave,
@@ -184,31 +145,44 @@ export const useSheetAutoSave = (
isCalculating,
isSaving,
templateId,
memoizedGetAllModifiedCells,
getAllModifiedCells,
performSave,
])
// Ручное сохранение
const manualSave = useCallback(() => {
const currentData = memoizedGetAllModifiedCells()
performSave(currentData)
}, [memoizedGetAllModifiedCells, performSave])
const currentData = getAllModifiedCells()
if (Object.keys(currentData).length > 0) {
performSave(currentData)
}
}, [getAllModifiedCells, performSave])
// Функция загрузки сохраненных листов
// Функция загрузки сохраненных листов (теперь используем React Query кеш)
const loadSavedSheets = useCallback((): Record<
string,
Record<string, any>
> => {
const result: Record<string, Record<string, any>> = {}
Object.entries(sheets).forEach(([sheetName, sheet]) => {
Object.entries(sheetsMap).forEach(([sheetName, sheet]) => {
if (sheet.cells && Object.keys(sheet.cells).length > 0) {
result[sheetName] = sheet.cells
}
})
return result
}, [sheets])
}, [sheetsMap])
// Функция загрузки листов (используем refetch из React Query)
const loadSheets = useCallback(async () => {
const result = await refetchSheets()
return (
result.data?.sheets.reduce<Record<string, ApiSheet>>((acc, sheet) => {
acc[sheet.name] = sheet
return acc
}, {}) || {}
)
}, [refetchSheets])
// Вычисляемые значения
const isDataChanged = revision > lastSavedRevision
@@ -222,7 +196,7 @@ export const useSheetAutoSave = (
hasUnsavedChanges,
lastSaveError,
lastSavedRevision,
sheets,
sheets: sheetsMap,
// Методы
manualSave,
@@ -230,7 +204,7 @@ export const useSheetAutoSave = (
loadSheets,
// Утилиты
getCurrentData: memoizedGetAllModifiedCells,
getCurrentData: getAllModifiedCells,
clearError: () => setLastSaveError(null),
}
}

196
src/hook/useSheetQueries.ts Normal file
View File

@@ -0,0 +1,196 @@
import {
createSheet,
getSheet,
getSheets,
getSheetsByTemplate,
patchSheet,
type ApiSheet,
type GetSheetsOutput,
type PatchSheetInput,
} from '@/service/sheetApiService'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
// Ключи для React Query
export const sheetKeys = {
all: ['sheets'] as const,
byTemplate: (templateId: string) =>
[...sheetKeys.all, 'template', templateId] as const,
detail: (sheetId: string) => [...sheetKeys.all, 'detail', sheetId] as const,
}
// Хук для получения всех листов
export function useSheets() {
return useQuery({
queryKey: sheetKeys.all,
queryFn: getSheets,
staleTime: 2 * 60 * 1000, // 2 минуты
gcTime: 10 * 60 * 1000, // 10 минут кеш
})
}
// Хук для получения листов по template_id с кешированием
export function useSheetsByTemplate(templateId: string | undefined) {
return useQuery({
queryKey: sheetKeys.byTemplate(templateId || ''),
queryFn: () => getSheetsByTemplate(templateId!),
enabled: !!templateId,
staleTime: 1 * 60 * 1000, // 1 минута для активных данных
gcTime: 5 * 60 * 1000, // 5 минут кеш
refetchOnWindowFocus: false, // Не перезапрашивать при фокусе окна
})
}
// Хук для получения конкретного листа
export function useSheet(sheetId: string | undefined) {
return useQuery({
queryKey: sheetKeys.detail(sheetId || ''),
queryFn: () => getSheet(sheetId!),
enabled: !!sheetId,
staleTime: 5 * 60 * 1000, // 5 минут
gcTime: 10 * 60 * 1000, // 10 минут кеш
})
}
// Хук для создания листа
export function useCreateSheet() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: createSheet,
onSuccess: (data, variables) => {
// Инвалидируем кеши после создания листа
queryClient.invalidateQueries({
queryKey: sheetKeys.byTemplate(variables.template_id),
})
queryClient.invalidateQueries({
queryKey: sheetKeys.all,
})
// Добавляем новый лист в кеш списка листов для шаблона
queryClient.setQueryData<GetSheetsOutput>(
sheetKeys.byTemplate(variables.template_id),
oldData => {
if (!oldData) return { sheets: [] }
const newSheet: ApiSheet = {
id: data.id,
name: variables.name,
cells: variables.cells,
template_id: variables.template_id,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
}
return {
sheets: [...oldData.sheets, newSheet],
}
}
)
},
})
}
// Хук для обновления листа
export function usePatchSheet() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({
sheetId,
data,
}: {
sheetId: string
data: PatchSheetInput
}) => patchSheet(sheetId, data),
onSuccess: (result, variables) => {
// Обновляем конкретный лист в кеше
queryClient.setQueryData(
sheetKeys.detail(variables.sheetId),
(oldData: any) => {
if (!oldData?.sheet) return oldData
return {
sheet: {
...oldData.sheet,
...variables.data,
updated_at: new Date().toISOString(),
},
}
}
)
// Обновляем лист в списке листов для шаблона
const sheetDetail = queryClient.getQueryData(
sheetKeys.detail(variables.sheetId)
) as any
const templateId = sheetDetail?.sheet?.template_id
if (templateId) {
queryClient.setQueryData<GetSheetsOutput>(
sheetKeys.byTemplate(templateId),
oldData => {
if (!oldData) return oldData
return {
sheets: oldData.sheets.map(sheet =>
sheet.id === variables.sheetId
? {
...sheet,
name: variables.data.name || sheet.name,
cells: variables.data.cells || sheet.cells,
updated_at: new Date().toISOString(),
}
: sheet
),
}
}
)
}
},
})
}
// Хук для массового обновления листов (для автосохранения)
export function useBatchUpdateSheets() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (
updates: Array<{
sheetId: string
data: PatchSheetInput
templateId: string
}>
) => {
// Выполняем все обновления параллельно
const results = await Promise.allSettled(
updates.map(({ sheetId, data }) => patchSheet(sheetId, data))
)
return results
},
onSuccess: (results, variables) => {
// Обновляем кеши для всех затронутых шаблонов
const templateIds = new Set(variables.map(v => v.templateId))
templateIds.forEach(templateId => {
queryClient.invalidateQueries({
queryKey: sheetKeys.byTemplate(templateId),
})
})
},
})
}
// Хук для предзагрузки листов шаблона
export function usePrefetchSheetsByTemplate() {
const queryClient = useQueryClient()
return (templateId: string) => {
queryClient.prefetchQuery({
queryKey: sheetKeys.byTemplate(templateId),
queryFn: () => getSheetsByTemplate(templateId),
staleTime: 1 * 60 * 1000,
})
}
}