удаление неиспользуемого
This commit is contained in:
@@ -2,6 +2,7 @@ import { FC, useEffect } from 'react'
|
|||||||
|
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||||
|
|
||||||
|
import Layout from '@/app/Layout'
|
||||||
import { CategoryProvider } from '@/entity/template/model/CategoryContext'
|
import { CategoryProvider } from '@/entity/template/model/CategoryContext'
|
||||||
import { TemplateProvider } from '@/entity/template/model/TemplateContext'
|
import { TemplateProvider } from '@/entity/template/model/TemplateContext'
|
||||||
import {
|
import {
|
||||||
@@ -17,7 +18,6 @@ import {
|
|||||||
TemplatesOverviewPage,
|
TemplatesOverviewPage,
|
||||||
} from '@/page'
|
} from '@/page'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { Layout } from './Layout'
|
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Outlet } from 'react-router-dom'
|
|||||||
const Layout: FC = () => {
|
const Layout: FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col bg-gray-50">
|
<div className="flex h-screen flex-col bg-gray-50">
|
||||||
{/* Основной контент */}
|
|
||||||
<main className="flex-1 overflow-auto">
|
<main className="flex-1 overflow-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import Layout from "./Layout";
|
|
||||||
|
|
||||||
export { Layout };
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import type { TypedUseSelectorHook } from "react-redux";
|
|
||||||
import type { RootState, AppDispatch } from "./store";
|
|
||||||
|
|
||||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
|
||||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
|
||||||
@@ -9,7 +9,6 @@ import { initializeElementRegistry } from '@/entity/element/model/interface'
|
|||||||
import App from './App'
|
import App from './App'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
// Инициализируем реестр элементов
|
|
||||||
initializeElementRegistry()
|
initializeElementRegistry()
|
||||||
|
|
||||||
createRoot(document.getElementById('root') as HTMLElement).render(
|
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
|||||||
@@ -32,12 +32,6 @@ export interface CellSelection {
|
|||||||
endCol: number
|
endCol: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// Интерфейс для выделения множества ячеек
|
|
||||||
export interface MultiSelection {
|
|
||||||
sheet: SheetType
|
|
||||||
selections: CellSelection[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Кэшированные данные ячеек для оптимизации
|
// Кэшированные данные ячеек для оптимизации
|
||||||
export interface CachedCellData {
|
export interface CachedCellData {
|
||||||
displayValue: string
|
displayValue: string
|
||||||
|
|||||||
@@ -13,14 +13,11 @@ import {
|
|||||||
UpdateDailyConditionOutput,
|
UpdateDailyConditionOutput,
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
// Базовый URL для API (может быть настроен через переменные окружения)
|
|
||||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'
|
|
||||||
|
|
||||||
// API для лабораторий
|
// API для лабораторий
|
||||||
export const laboratoriesApi = {
|
export const laboratoriesApi = {
|
||||||
// Получить все лаборатории
|
// Получить все лаборатории
|
||||||
getAll: async (): Promise<Laboratory[]> => {
|
getAll: async (): Promise<Laboratory[]> => {
|
||||||
const response = await fetch(`${BASE_URL}/laboratories/`)
|
const response = await fetch(`api/laboratories/`)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Ошибка загрузки лабораторий')
|
throw new Error('Ошибка загрузки лабораторий')
|
||||||
}
|
}
|
||||||
@@ -30,7 +27,7 @@ export const laboratoriesApi = {
|
|||||||
|
|
||||||
// Получить лабораторию по ID
|
// Получить лабораторию по ID
|
||||||
getById: async (id: string): Promise<Laboratory | null> => {
|
getById: async (id: string): Promise<Laboratory | null> => {
|
||||||
const response = await fetch(`${BASE_URL}/laboratories/${id}`)
|
const response = await fetch(`api/laboratories/${id}`)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
return null
|
return null
|
||||||
@@ -60,8 +57,8 @@ export const dailyConditionsApi = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = searchParams.toString()
|
const url = searchParams.toString()
|
||||||
? `${BASE_URL}/daily-conditions/?${searchParams}`
|
? `api/daily-conditions/?${searchParams}`
|
||||||
: `${BASE_URL}/daily-conditions/`
|
: `api/daily-conditions/`
|
||||||
|
|
||||||
const response = await fetch(url)
|
const response = await fetch(url)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -73,7 +70,7 @@ export const dailyConditionsApi = {
|
|||||||
|
|
||||||
// Получить конкретное условие по ID
|
// Получить конкретное условие по ID
|
||||||
getById: async (id: string): Promise<DailyCondition | null> => {
|
getById: async (id: string): Promise<DailyCondition | null> => {
|
||||||
const response = await fetch(`${BASE_URL}/daily-conditions/${id}`)
|
const response = await fetch(`api/daily-conditions/${id}`)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
return null
|
return null
|
||||||
@@ -86,7 +83,7 @@ export const dailyConditionsApi = {
|
|||||||
|
|
||||||
// Создать новые условия
|
// Создать новые условия
|
||||||
create: async (input: CreateDailyConditionInput): Promise<string> => {
|
create: async (input: CreateDailyConditionInput): Promise<string> => {
|
||||||
const response = await fetch(`${BASE_URL}/daily-conditions/`, {
|
const response = await fetch(`api/daily-conditions/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -108,7 +105,7 @@ export const dailyConditionsApi = {
|
|||||||
id: string,
|
id: string,
|
||||||
input: UpdateDailyConditionInput
|
input: UpdateDailyConditionInput
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
const response = await fetch(`${BASE_URL}/daily-conditions/${id}`, {
|
const response = await fetch(`api/daily-conditions/${id}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -124,7 +121,7 @@ export const dailyConditionsApi = {
|
|||||||
|
|
||||||
// Удалить условия
|
// Удалить условия
|
||||||
delete: async (id: string): Promise<void> => {
|
delete: async (id: string): Promise<void> => {
|
||||||
const response = await fetch(`${BASE_URL}/daily-conditions/${id}`, {
|
const response = await fetch(`api/daily-conditions/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -203,56 +200,3 @@ export const elementApi = {
|
|||||||
return savedCondition
|
return savedCondition
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вспомогательные функции
|
|
||||||
export const verificationConditionsUtils = {
|
|
||||||
// Валидация значения условия
|
|
||||||
validateConditionValue: (
|
|
||||||
value: any,
|
|
||||||
dataType: string = 'string',
|
|
||||||
minValue?: number,
|
|
||||||
maxValue?: number
|
|
||||||
): { isValid: boolean; error?: string } => {
|
|
||||||
if (dataType === 'number') {
|
|
||||||
const numValue = parseFloat(value)
|
|
||||||
if (isNaN(numValue)) {
|
|
||||||
return { isValid: false, error: 'Должно быть числом' }
|
|
||||||
}
|
|
||||||
if (minValue !== undefined && numValue < minValue) {
|
|
||||||
return { isValid: false, error: `Минимальное значение: ${minValue}` }
|
|
||||||
}
|
|
||||||
if (maxValue !== undefined && numValue > maxValue) {
|
|
||||||
return { isValid: false, error: `Максимальное значение: ${maxValue}` }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { isValid: true }
|
|
||||||
},
|
|
||||||
|
|
||||||
// Форматирование значения с единицей измерения
|
|
||||||
formatValueWithUnit: (value: any, unit?: string): string => {
|
|
||||||
if (value === undefined || value === null || value === '') {
|
|
||||||
return '-'
|
|
||||||
}
|
|
||||||
return unit ? `${value} ${unit}` : value.toString()
|
|
||||||
},
|
|
||||||
|
|
||||||
// Получение ключа условия для хранения в conditions JSON (обычно используется ID типа условия)
|
|
||||||
getConditionKey: (conditionTypeId: string): string => {
|
|
||||||
return conditionTypeId
|
|
||||||
},
|
|
||||||
|
|
||||||
// Генерация ключа на основе названия условия (для обратной совместимости)
|
|
||||||
generateConditionKey: (conditionTypeName: string): string => {
|
|
||||||
return conditionTypeName
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\s+/g, '_')
|
|
||||||
.replace(/[^\w]/g, '')
|
|
||||||
},
|
|
||||||
|
|
||||||
// Проверка, является ли строка валидным UUID
|
|
||||||
isValidUUID: (str: string): boolean => {
|
|
||||||
const uuidRegex =
|
|
||||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
||||||
return uuidRegex.test(str)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { VerificationConditionsEditor } from './VerificationConditionsEditor'
|
|||||||
import { VerificationConditionsPreview } from './VerificationConditionsPreview'
|
import { VerificationConditionsPreview } from './VerificationConditionsPreview'
|
||||||
import { VerificationConditionsRender } from './VerificationConditionsRender'
|
import { VerificationConditionsRender } from './VerificationConditionsRender'
|
||||||
|
|
||||||
// Форматирование даты как в функции ДАТАПРОТОКОЛА
|
|
||||||
const formatVerificationDate = (dateStr: string): string => {
|
const formatVerificationDate = (dateStr: string): string => {
|
||||||
try {
|
try {
|
||||||
const months = [
|
const months = [
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import {
|
|||||||
VOLATILE_FUNCTIONS,
|
VOLATILE_FUNCTIONS,
|
||||||
} from './functions'
|
} from './functions'
|
||||||
|
|
||||||
// Регулярные выражения для ссылок
|
|
||||||
// Ограничиваем поиск ячеек диапазоном A1-Z90
|
// Ограничиваем поиск ячеек диапазоном A1-Z90
|
||||||
const QUALIFIED_REF_RE = /\b[A-Za-z0-9_]+\b/g // SheetName!A1 до SheetName!Z90
|
const QUALIFIED_REF_RE = /\b[A-Za-z0-9_]+\b/g
|
||||||
const LOCAL_REF_RE = /\b[A-Z]([1-9]|[1-8][0-9]|90)\b/g // A1 до Z90
|
const LOCAL_REF_RE = /\b[A-Z]([1-9]|[1-8][0-9]|90)\b/g
|
||||||
const RANGE_RE = /\b[A-Z]([1-9]|[1-8][0-9]|90):[A-Z]([1-9]|[1-8][0-9]|90)\b/g // A1:B2, D5:D7, etc. в диапазоне A1-Z90
|
const RANGE_RE = /\b[A-Z]([1-9]|[1-8][0-9]|90):[A-Z]([1-9]|[1-8][0-9]|90)\b/g
|
||||||
const QUALIFIED_RANGE_RE =
|
const QUALIFIED_RANGE_RE =
|
||||||
/\b[A-Za-z0-9_]+:[A-Z]([1-9]|[1-8][0-9]|90)\b/g // SheetName!A1:B2 в диапазоне A1-Z90
|
/\b[A-Za-z0-9_]+:[A-Z]([1-9]|[1-8][0-9]|90)\b/g
|
||||||
|
|
||||||
// Типы для значений ячеек
|
// Типы для значений ячеек
|
||||||
export type CellValue = string | number | boolean | null | undefined
|
export type CellValue = string | number | boolean | null | undefined
|
||||||
@@ -598,10 +597,6 @@ export class Workbook {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFunction(name: string): void {
|
|
||||||
delete this._functions[name.toUpperCase()]
|
|
||||||
}
|
|
||||||
|
|
||||||
getFunctions(): Record<string, ExcelFunction> {
|
getFunctions(): Record<string, ExcelFunction> {
|
||||||
return this._functions
|
return this._functions
|
||||||
}
|
}
|
||||||
@@ -641,14 +636,6 @@ export class Workbook {
|
|||||||
|
|
||||||
// Утилиты для работы с координатами ячеек
|
// Утилиты для работы с координатами ячеек
|
||||||
export class CellUtils {
|
export class CellUtils {
|
||||||
static columnToIndex(column: string): number {
|
|
||||||
let result = 0
|
|
||||||
for (let i = 0; i < column.length; i++) {
|
|
||||||
result = result * 26 + (column.charCodeAt(i) - 65 + 1)
|
|
||||||
}
|
|
||||||
return result - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
static indexToColumn(index: number): string {
|
static indexToColumn(index: number): string {
|
||||||
let result = ''
|
let result = ''
|
||||||
while (index >= 0) {
|
while (index >= 0) {
|
||||||
@@ -658,17 +645,6 @@ export class CellUtils {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
static parseCellName(cellName: string): { column: string; row: number } {
|
|
||||||
const match = cellName.match(/^([A-Z]+)(\d+)$/)
|
|
||||||
if (!match) {
|
|
||||||
throw new Error(`Invalid cell name: ${cellName}`)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
column: match[1],
|
|
||||||
row: parseInt(match[2], 10),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static createCellName(column: string | number, row: number): string {
|
static createCellName(column: string | number, row: number): string {
|
||||||
const col =
|
const col =
|
||||||
typeof column === 'number' ? CellUtils.indexToColumn(column) : column
|
typeof column === 'number' ? CellUtils.indexToColumn(column) : column
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import {
|
import { getFileData, getLatestFileForTemplate } from '@/service/fileApiService'
|
||||||
getFileData,
|
import { useQuery } from '@tanstack/react-query'
|
||||||
getFilesByTemplateId,
|
|
||||||
getLatestFileForTemplate,
|
|
||||||
parseExcelFile,
|
|
||||||
} from '@/service/fileApiService'
|
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
// Ключи для React Query
|
// Ключи для React Query
|
||||||
export const fileKeys = {
|
export const fileKeys = {
|
||||||
@@ -16,17 +11,6 @@ export const fileKeys = {
|
|||||||
data: (fileId: string) => [...fileKeys.all, 'data', fileId] 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) {
|
export function useLatestFileForTemplate(templateId: string | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -48,38 +32,3 @@ export function useFileData(fileId: string | undefined) {
|
|||||||
gcTime: 30 * 60 * 1000, // 30 минут кеш
|
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -69,14 +69,7 @@ export function useMultiSheetEngine({
|
|||||||
if (!workbookRef.current) return
|
if (!workbookRef.current) return
|
||||||
|
|
||||||
// Предотвращаем случайную очистку данных при смене вкладки
|
// Предотвращаем случайную очистку данных при смене вкладки
|
||||||
if (document.hidden) {
|
if (document.hidden) return
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
console.log(
|
|
||||||
'⚠️ useMultiSheetEngine: предотвращаем загрузку данных когда страница скрыта'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем ключ для проверки изменений данных
|
// Создаем ключ для проверки изменений данных
|
||||||
const dataKey = JSON.stringify(initialData)
|
const dataKey = JSON.stringify(initialData)
|
||||||
@@ -85,21 +78,10 @@ export function useMultiSheetEngine({
|
|||||||
if (
|
if (
|
||||||
loadedDataRef.current === dataKey &&
|
loadedDataRef.current === dataKey &&
|
||||||
Object.keys(initialData).length > 0
|
Object.keys(initialData).length > 0
|
||||||
) {
|
)
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
console.log('⏭️ useMultiSheetEngine: данные уже загружены, пропускаем')
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(initialData).length > 0) {
|
if (Object.keys(initialData).length > 0) {
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
console.log(
|
|
||||||
'🔄 useMultiSheetEngine: загрузка initialData:',
|
|
||||||
Object.keys(initialData)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Очищаем только те листы, для которых есть новые данные
|
// Очищаем только те листы, для которых есть новые данные
|
||||||
for (const sheetName of Object.keys(initialData)) {
|
for (const sheetName of Object.keys(initialData)) {
|
||||||
const sheet = workbookRef.current.getSheet(sheetName)
|
const sheet = workbookRef.current.getSheet(sheetName)
|
||||||
@@ -117,42 +99,6 @@ export function useMultiSheetEngine({
|
|||||||
}
|
}
|
||||||
}, [initialData, safeRecalc])
|
}, [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(() => {
|
const debouncedRecalc = useCallback(() => {
|
||||||
if (!workbookRef.current || isRecalculatingRef.current) return
|
if (!workbookRef.current || isRecalculatingRef.current) return
|
||||||
@@ -276,15 +222,6 @@ export function useMultiSheetEngine({
|
|||||||
const setTemplateData = useCallback(
|
const setTemplateData = useCallback(
|
||||||
(data: Record<string, Record<string, any>>) => {
|
(data: Record<string, Record<string, any>>) => {
|
||||||
templateDataRef.current = JSON.parse(JSON.stringify(data)) // Глубокая копия
|
templateDataRef.current = JSON.parse(JSON.stringify(data)) // Глубокая копия
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
console.log('📋 Установлены базовые данные шаблона:', {
|
|
||||||
sheets: Object.keys(templateDataRef.current),
|
|
||||||
cellCount: Object.values(templateDataRef.current).reduce(
|
|
||||||
(total, sheet) => total + Object.keys(sheet).length,
|
|
||||||
0
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@@ -333,16 +270,6 @@ export function useMultiSheetEngine({
|
|||||||
if (normalizedCurrent !== normalizedTemplate) {
|
if (normalizedCurrent !== normalizedTemplate) {
|
||||||
// Если текущего значения нет (ячейка очищена), отправляем ''
|
// Если текущего значения нет (ячейка очищена), отправляем ''
|
||||||
modifiedSheet[cellAddress] = currentVal
|
modifiedSheet[cellAddress] = currentVal
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
console.log(
|
|
||||||
`🔧 Пользовательское изменение ${sheetName}:${cellAddress}:`,
|
|
||||||
{
|
|
||||||
template: normalizedTemplate,
|
|
||||||
current: normalizedCurrent,
|
|
||||||
value: currentVal,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -351,22 +278,6 @@ export function useMultiSheetEngine({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
const totalModified = Object.values(modifiedData).reduce(
|
|
||||||
(total, sheet) => total + Object.keys(sheet).length,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
console.log(`📊 Найдено ${totalModified} измененных ячеек:`, {
|
|
||||||
sheets: Object.keys(modifiedData),
|
|
||||||
details: Object.fromEntries(
|
|
||||||
Object.entries(modifiedData).map(([sheetName, cells]) => [
|
|
||||||
sheetName,
|
|
||||||
Object.keys(cells).length,
|
|
||||||
])
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return modifiedData
|
return modifiedData
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -405,7 +316,6 @@ export function useMultiSheetEngine({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// sheetCells убран
|
|
||||||
setCellValue,
|
setCellValue,
|
||||||
setCellValueWithoutRecalc,
|
setCellValueWithoutRecalc,
|
||||||
debouncedRecalc,
|
debouncedRecalc,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
createSheet,
|
createSheet,
|
||||||
getSheet,
|
|
||||||
getSheets,
|
|
||||||
getSheetsByTemplate,
|
getSheetsByTemplate,
|
||||||
patchSheet,
|
patchSheet,
|
||||||
type ApiSheet,
|
type ApiSheet,
|
||||||
@@ -18,16 +16,6 @@ export const sheetKeys = {
|
|||||||
detail: (sheetId: string) => [...sheetKeys.all, 'detail', sheetId] 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 с кешированием
|
// Хук для получения листов по template_id с кешированием
|
||||||
export function useSheetsByTemplate(templateId: string | undefined) {
|
export function useSheetsByTemplate(templateId: string | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -40,17 +28,6 @@ export function useSheetsByTemplate(templateId: string | undefined) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Хук для получения конкретного листа
|
|
||||||
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() {
|
export function useCreateSheet() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -149,48 +126,3 @@ export function usePatchSheet() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Хук для массового обновления листов (для автосохранения)
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,335 +0,0 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
import {
|
|
||||||
CellUtils,
|
|
||||||
CellValue,
|
|
||||||
Workbook,
|
|
||||||
} from '../feature/excelEngine/excelEngine'
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user