удаление неиспользуемого
This commit is contained in:
@@ -2,6 +2,7 @@ import { FC, useEffect } from 'react'
|
||||
|
||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||
|
||||
import Layout from '@/app/Layout'
|
||||
import { CategoryProvider } from '@/entity/template/model/CategoryContext'
|
||||
import { TemplateProvider } from '@/entity/template/model/TemplateContext'
|
||||
import {
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
TemplatesOverviewPage,
|
||||
} from '@/page'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { Layout } from './Layout'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Outlet } from 'react-router-dom'
|
||||
const Layout: FC = () => {
|
||||
return (
|
||||
<div className="flex h-screen flex-col bg-gray-50">
|
||||
{/* Основной контент */}
|
||||
<main className="flex-1 overflow-auto">
|
||||
<Outlet />
|
||||
</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 './index.css'
|
||||
|
||||
// Инициализируем реестр элементов
|
||||
initializeElementRegistry()
|
||||
|
||||
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
|
||||
@@ -32,12 +32,6 @@ export interface CellSelection {
|
||||
endCol: number
|
||||
}
|
||||
|
||||
// Интерфейс для выделения множества ячеек
|
||||
export interface MultiSelection {
|
||||
sheet: SheetType
|
||||
selections: CellSelection[]
|
||||
}
|
||||
|
||||
// Кэшированные данные ячеек для оптимизации
|
||||
export interface CachedCellData {
|
||||
displayValue: string
|
||||
|
||||
@@ -13,14 +13,11 @@ import {
|
||||
UpdateDailyConditionOutput,
|
||||
} from './types'
|
||||
|
||||
// Базовый URL для API (может быть настроен через переменные окружения)
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'
|
||||
|
||||
// API для лабораторий
|
||||
export const laboratoriesApi = {
|
||||
// Получить все лаборатории
|
||||
getAll: async (): Promise<Laboratory[]> => {
|
||||
const response = await fetch(`${BASE_URL}/laboratories/`)
|
||||
const response = await fetch(`api/laboratories/`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка загрузки лабораторий')
|
||||
}
|
||||
@@ -30,7 +27,7 @@ export const laboratoriesApi = {
|
||||
|
||||
// Получить лабораторию по ID
|
||||
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.status === 404) {
|
||||
return null
|
||||
@@ -60,8 +57,8 @@ export const dailyConditionsApi = {
|
||||
}
|
||||
|
||||
const url = searchParams.toString()
|
||||
? `${BASE_URL}/daily-conditions/?${searchParams}`
|
||||
: `${BASE_URL}/daily-conditions/`
|
||||
? `api/daily-conditions/?${searchParams}`
|
||||
: `api/daily-conditions/`
|
||||
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
@@ -73,7 +70,7 @@ export const dailyConditionsApi = {
|
||||
|
||||
// Получить конкретное условие по ID
|
||||
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.status === 404) {
|
||||
return null
|
||||
@@ -86,7 +83,7 @@ export const dailyConditionsApi = {
|
||||
|
||||
// Создать новые условия
|
||||
create: async (input: CreateDailyConditionInput): Promise<string> => {
|
||||
const response = await fetch(`${BASE_URL}/daily-conditions/`, {
|
||||
const response = await fetch(`api/daily-conditions/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -108,7 +105,7 @@ export const dailyConditionsApi = {
|
||||
id: string,
|
||||
input: UpdateDailyConditionInput
|
||||
): Promise<string> => {
|
||||
const response = await fetch(`${BASE_URL}/daily-conditions/${id}`, {
|
||||
const response = await fetch(`api/daily-conditions/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -124,7 +121,7 @@ export const dailyConditionsApi = {
|
||||
|
||||
// Удалить условия
|
||||
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',
|
||||
})
|
||||
if (!response.ok) {
|
||||
@@ -203,56 +200,3 @@ export const elementApi = {
|
||||
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 { VerificationConditionsRender } from './VerificationConditionsRender'
|
||||
|
||||
// Форматирование даты как в функции ДАТАПРОТОКОЛА
|
||||
const formatVerificationDate = (dateStr: string): string => {
|
||||
try {
|
||||
const months = [
|
||||
|
||||
@@ -4,13 +4,12 @@ import {
|
||||
VOLATILE_FUNCTIONS,
|
||||
} from './functions'
|
||||
|
||||
// Регулярные выражения для ссылок
|
||||
// Ограничиваем поиск ячеек диапазоном A1-Z90
|
||||
const QUALIFIED_REF_RE = /\b[A-Za-z0-9_]+\b/g // SheetName!A1 до SheetName!Z90
|
||||
const LOCAL_REF_RE = /\b[A-Z]([1-9]|[1-8][0-9]|90)\b/g // 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 // A1:B2, D5:D7, etc. в диапазоне A1-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
|
||||
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 =
|
||||
/\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
|
||||
@@ -598,10 +597,6 @@ export class Workbook {
|
||||
}
|
||||
}
|
||||
|
||||
removeFunction(name: string): void {
|
||||
delete this._functions[name.toUpperCase()]
|
||||
}
|
||||
|
||||
getFunctions(): Record<string, ExcelFunction> {
|
||||
return this._functions
|
||||
}
|
||||
@@ -641,14 +636,6 @@ export class Workbook {
|
||||
|
||||
// Утилиты для работы с координатами ячеек
|
||||
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 {
|
||||
let result = ''
|
||||
while (index >= 0) {
|
||||
@@ -658,17 +645,6 @@ export class CellUtils {
|
||||
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 {
|
||||
const col =
|
||||
typeof column === 'number' ? CellUtils.indexToColumn(column) : column
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import {
|
||||
getFileData,
|
||||
getFilesByTemplateId,
|
||||
getLatestFileForTemplate,
|
||||
parseExcelFile,
|
||||
} from '@/service/fileApiService'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { getFileData, getLatestFileForTemplate } from '@/service/fileApiService'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
// Ключи для React Query
|
||||
export const fileKeys = {
|
||||
@@ -16,17 +11,6 @@ export const fileKeys = {
|
||||
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({
|
||||
@@ -48,38 +32,3 @@ export function useFileData(fileId: string | undefined) {
|
||||
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 (document.hidden) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log(
|
||||
'⚠️ useMultiSheetEngine: предотвращаем загрузку данных когда страница скрыта'
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (document.hidden) return
|
||||
|
||||
// Создаем ключ для проверки изменений данных
|
||||
const dataKey = JSON.stringify(initialData)
|
||||
@@ -85,21 +78,10 @@ export function useMultiSheetEngine({
|
||||
if (
|
||||
loadedDataRef.current === dataKey &&
|
||||
Object.keys(initialData).length > 0
|
||||
) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('⏭️ useMultiSheetEngine: данные уже загружены, пропускаем')
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
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)) {
|
||||
const sheet = workbookRef.current.getSheet(sheetName)
|
||||
@@ -117,42 +99,6 @@ export function useMultiSheetEngine({
|
||||
}
|
||||
}, [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
|
||||
@@ -276,15 +222,6 @@ export function useMultiSheetEngine({
|
||||
const setTemplateData = useCallback(
|
||||
(data: Record<string, Record<string, any>>) => {
|
||||
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) {
|
||||
// Если текущего значения нет (ячейка очищена), отправляем ''
|
||||
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
|
||||
}, [])
|
||||
|
||||
@@ -405,7 +316,6 @@ export function useMultiSheetEngine({
|
||||
}, [])
|
||||
|
||||
return {
|
||||
// sheetCells убран
|
||||
setCellValue,
|
||||
setCellValueWithoutRecalc,
|
||||
debouncedRecalc,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
createSheet,
|
||||
getSheet,
|
||||
getSheets,
|
||||
getSheetsByTemplate,
|
||||
patchSheet,
|
||||
type ApiSheet,
|
||||
@@ -18,16 +16,6 @@ export const sheetKeys = {
|
||||
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({
|
||||
@@ -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() {
|
||||
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