починка багов
This commit is contained in:
85
src/hook/useFileQueries.ts
Normal file
85
src/hook/useFileQueries.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -192,10 +192,10 @@ export function useMultiSheetEngine({
|
||||
}
|
||||
}
|
||||
|
||||
// Запускаем безопасный пересчет
|
||||
safeRecalc()
|
||||
// Запускаем отложенный пересчет с дебаунсом (избегаем двойного пересчёта)
|
||||
debouncedRecalc()
|
||||
},
|
||||
[safeRecalc]
|
||||
[debouncedRecalc]
|
||||
)
|
||||
|
||||
// Получение значения ячейки (вычисленное)
|
||||
|
||||
@@ -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
196
src/hook/useSheetQueries.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user