+ mapToCellValues: (config, value) => {
+ // Используем targetCells из конфигурации
+ const cells = config.targetCells || []
+ const cellMapping = config.cellMapping || {}
+
+ // Если значение не объект, возвращаем пустые значения
+ if (!value || typeof value !== 'object') {
+ return cells.map(target => ({
+ target,
+ value: '',
+ }))
+ }
+
+ // Создаем обратное сопоставление: индекс ячейки -> параметр
+ const reversedMapping: { [cellIndex: string]: string } = {}
+ Object.entries(cellMapping).forEach(([param, cellIndex]) => {
+ reversedMapping[cellIndex] = param
+ })
+
+ return cells.map((target, idx) => {
+ const paramKey = reversedMapping[idx.toString()]
+ const paramValue = paramKey ? (value as any)[paramKey] : ''
+
+ return {
+ target,
+ value: paramValue || '',
+ }
+ })
+ },
+ Editor: ({ config, onChange }) => {
+ const availableParams = [
+ { key: 'temperature', label: 'Температура' },
+ { key: 'humidity', label: 'Влажность' },
+ { key: 'pressure', label: 'Давление' },
+ { key: 'voltage', label: 'Напряжение' },
+ { key: 'frequency', label: 'Частота' },
+ { key: 'lastUpdated', label: 'Дата обновления' },
+ ]
+
+ const targetCells = config.targetCells || []
+ const cellMapping = config.cellMapping || {}
+
+ // Функция для создания автоматического сопоставления по порядку
+ const createAutoMapping = () => {
+ const newMapping: { [key: string]: string } = {}
+ const paramKeys = availableParams.map(p => p.key)
+
+ // Сопоставляем первые N параметров с первыми N ячейками
+ targetCells.forEach((_, index) => {
+ if (index < paramKeys.length) {
+ newMapping[paramKeys[index]] = index.toString()
+ }
+ })
+
+ onChange({
+ ...config,
+ cellMapping: newMapping,
+ })
+ }
+
+ const updateCellMapping = (param: string, cellIndex: string) => {
+ const newMapping = { ...cellMapping }
+ if (cellIndex === '' || cellIndex === 'none') {
+ delete newMapping[param]
+ } else {
+ newMapping[param] = cellIndex
+ }
+ onChange({
+ ...config,
+ cellMapping: newMapping,
+ })
+ }
+
+ return (
+
+
+
+ Элемент "Условия калибровки" позволяет вводить и отслеживать
+ параметры окружающей среды при проведении измерений. Поддерживает
+ валидацию значений и отслеживание времени обновления.
+
+
+
+ {/* Сопоставление параметров с ячейками */}
+ {targetCells.length > 0 && (
-
- Температура (°C)
-
-
- onChange({
- ...config,
- defaultConditions: {
- ...config.defaultConditions,
- temperature: e.target.value,
- humidity: config.defaultConditions?.humidity || '65',
- pressure: config.defaultConditions?.pressure || '101.3',
- voltage: config.defaultConditions?.voltage || '220',
- frequency: config.defaultConditions?.frequency || '50',
- lastUpdated:
- config.defaultConditions?.lastUpdated ||
- new Date().toISOString(),
- },
- })
- }
- placeholder="20"
- />
+
+
+ Сопоставление с ячейками
+
+
+ Автозаполнение
+
+
+
+ {availableParams.map(param => (
+
+
+ {param.label}
+
+
+
+ updateCellMapping(param.key, value)
+ }
+ >
+
+
+
+
+ Не указано
+ {targetCells.map((cell, index) => (
+
+ {cell.sheet}!{cell.cell}{' '}
+ {cell.displayName ? `(${cell.displayName})` : ''}
+
+ ))}
+
+
+
+
+ ))}
+
+
+ Настройте какой параметр условий калибровки будет записан в какую
+ ячейку. Используйте кнопку "Автозаполнение" для автоматического
+ сопоставления по порядку.
+
-
-
- Влажность (%)
-
-
- onChange({
- ...config,
- defaultConditions: {
- ...config.defaultConditions,
- temperature: config.defaultConditions?.temperature || '20',
- humidity: e.target.value,
- pressure: config.defaultConditions?.pressure || '101.3',
- voltage: config.defaultConditions?.voltage || '220',
- frequency: config.defaultConditions?.frequency || '50',
- lastUpdated:
- config.defaultConditions?.lastUpdated ||
- new Date().toISOString(),
- },
- })
- }
- placeholder="65"
- />
-
-
-
- Давление (кПа)
-
-
- onChange({
- ...config,
- defaultConditions: {
- ...config.defaultConditions,
- temperature: config.defaultConditions?.temperature || '20',
- humidity: config.defaultConditions?.humidity || '65',
- pressure: e.target.value,
- voltage: config.defaultConditions?.voltage || '220',
- frequency: config.defaultConditions?.frequency || '50',
- lastUpdated:
- config.defaultConditions?.lastUpdated ||
- new Date().toISOString(),
- },
- })
- }
- placeholder="101.3"
- />
-
-
-
- Напряжение (В)
-
-
- onChange({
- ...config,
- defaultConditions: {
- ...config.defaultConditions,
- temperature: config.defaultConditions?.temperature || '20',
- humidity: config.defaultConditions?.humidity || '65',
- pressure: config.defaultConditions?.pressure || '101.3',
- voltage: e.target.value,
- frequency: config.defaultConditions?.frequency || '50',
- lastUpdated:
- config.defaultConditions?.lastUpdated ||
- new Date().toISOString(),
- },
- })
- }
- placeholder="220"
- />
-
-
-
- Частота (Гц)
-
-
- onChange({
- ...config,
- defaultConditions: {
- ...config.defaultConditions,
- temperature: config.defaultConditions?.temperature || '20',
- humidity: config.defaultConditions?.humidity || '65',
- pressure: config.defaultConditions?.pressure || '101.3',
- voltage: config.defaultConditions?.voltage || '220',
- frequency: e.target.value,
- lastUpdated:
- config.defaultConditions?.lastUpdated ||
- new Date().toISOString(),
- },
- })
- }
- placeholder="50"
- />
+ )}
+
+
+
Параметры по умолчанию
+
+
+
+ Температура (°C)
+
+
+ onChange({
+ ...config,
+ defaultConditions: {
+ ...config.defaultConditions,
+ temperature: e.target.value,
+ humidity: config.defaultConditions?.humidity || '65',
+ pressure: config.defaultConditions?.pressure || '101.3',
+ voltage: config.defaultConditions?.voltage || '220',
+ frequency: config.defaultConditions?.frequency || '50',
+ lastUpdated:
+ config.defaultConditions?.lastUpdated ||
+ new Date().toISOString(),
+ },
+ })
+ }
+ placeholder="20"
+ />
+
+
+
+ Влажность (%)
+
+
+ onChange({
+ ...config,
+ defaultConditions: {
+ ...config.defaultConditions,
+ temperature:
+ config.defaultConditions?.temperature || '20',
+ humidity: e.target.value,
+ pressure: config.defaultConditions?.pressure || '101.3',
+ voltage: config.defaultConditions?.voltage || '220',
+ frequency: config.defaultConditions?.frequency || '50',
+ lastUpdated:
+ config.defaultConditions?.lastUpdated ||
+ new Date().toISOString(),
+ },
+ })
+ }
+ placeholder="65"
+ />
+
+
+
+ Давление (кПа)
+
+
+ onChange({
+ ...config,
+ defaultConditions: {
+ ...config.defaultConditions,
+ temperature:
+ config.defaultConditions?.temperature || '20',
+ humidity: config.defaultConditions?.humidity || '65',
+ pressure: e.target.value,
+ voltage: config.defaultConditions?.voltage || '220',
+ frequency: config.defaultConditions?.frequency || '50',
+ lastUpdated:
+ config.defaultConditions?.lastUpdated ||
+ new Date().toISOString(),
+ },
+ })
+ }
+ placeholder="101.3"
+ />
+
+
+
+ Напряжение (В)
+
+
+ onChange({
+ ...config,
+ defaultConditions: {
+ ...config.defaultConditions,
+ temperature:
+ config.defaultConditions?.temperature || '20',
+ humidity: config.defaultConditions?.humidity || '65',
+ pressure: config.defaultConditions?.pressure || '101.3',
+ voltage: e.target.value,
+ frequency: config.defaultConditions?.frequency || '50',
+ lastUpdated:
+ config.defaultConditions?.lastUpdated ||
+ new Date().toISOString(),
+ },
+ })
+ }
+ placeholder="220"
+ />
+
+
+
+ Частота (Гц)
+
+
+ onChange({
+ ...config,
+ defaultConditions: {
+ ...config.defaultConditions,
+ temperature:
+ config.defaultConditions?.temperature || '20',
+ humidity: config.defaultConditions?.humidity || '65',
+ pressure: config.defaultConditions?.pressure || '101.3',
+ voltage: config.defaultConditions?.voltage || '220',
+ frequency: e.target.value,
+ lastUpdated:
+ config.defaultConditions?.lastUpdated ||
+ new Date().toISOString(),
+ },
+ })
+ }
+ placeholder="50"
+ />
+
-
- ),
+ )
+ },
Preview: ({ config }) => (
Предпросмотр
@@ -613,6 +749,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
),
Render: ({ config, value, onChange }) => {
+ // Если значение не установлено, используем defaultConditions из конфигурации
const conditions = value ||
config.defaultConditions || {
temperature: '20',
diff --git a/src/component/BasicElements/CheckboxElement.tsx b/src/component/BasicElements/CheckboxElement.tsx
index 089bc0f..f7802aa 100644
--- a/src/component/BasicElements/CheckboxElement.tsx
+++ b/src/component/BasicElements/CheckboxElement.tsx
@@ -18,6 +18,10 @@ export const checkboxDefinition: ElementDefinition
= {
targetCells: [],
},
mapToCells: config => config.targetCells || [],
+ mapToCellValues: (config, value) => {
+ const cells = config.targetCells || []
+ return cells.map(target => ({ target, value: value || false }))
+ },
Editor: () => (
Дополнительные настройки отсутствуют
diff --git a/src/component/BasicElements/DateElement.tsx b/src/component/BasicElements/DateElement.tsx
index e01c45a..38c95ba 100644
--- a/src/component/BasicElements/DateElement.tsx
+++ b/src/component/BasicElements/DateElement.tsx
@@ -17,6 +17,10 @@ export const dateDefinition: ElementDefinition
= {
targetCells: [],
},
mapToCells: config => config.targetCells || [],
+ mapToCellValues: (config, value) => {
+ const cells = config.targetCells || []
+ return cells.map(target => ({ target, value: value || '' }))
+ },
Editor: ({ config, onChange }) => (
diff --git a/src/component/BasicElements/NumberElement.tsx b/src/component/BasicElements/NumberElement.tsx
index 14b2e57..e9b434d 100644
--- a/src/component/BasicElements/NumberElement.tsx
+++ b/src/component/BasicElements/NumberElement.tsx
@@ -19,6 +19,10 @@ export const numberDefinition: ElementDefinition
= {
targetCells: [],
},
mapToCells: config => config.targetCells || [],
+ mapToCellValues: (config, value) => {
+ const cells = config.targetCells || []
+ return cells.map(target => ({ target, value: value || 0 }))
+ },
Editor: () => (
Дополнительные настройки отсутствуют
diff --git a/src/component/BasicElements/RadioElement.tsx b/src/component/BasicElements/RadioElement.tsx
index eea572e..b85a3a4 100644
--- a/src/component/BasicElements/RadioElement.tsx
+++ b/src/component/BasicElements/RadioElement.tsx
@@ -23,6 +23,10 @@ export const radioDefinition: ElementDefinition
= {
targetCells: [],
},
mapToCells: config => config.targetCells || [],
+ mapToCellValues: (config, value) => {
+ const cells = config.targetCells || []
+ return cells.map(target => ({ target, value: value || '' }))
+ },
Editor: ({ config, onChange }) => {
const handleAddOption = () => {
onChange({
diff --git a/src/component/BasicElements/SelectElement.tsx b/src/component/BasicElements/SelectElement.tsx
index dcb94c4..78ed474 100644
--- a/src/component/BasicElements/SelectElement.tsx
+++ b/src/component/BasicElements/SelectElement.tsx
@@ -30,6 +30,10 @@ export const selectDefinition: ElementDefinition = {
targetCells: [],
},
mapToCells: config => config.targetCells || [],
+ mapToCellValues: (config, value) => {
+ const cells = config.targetCells || []
+ return cells.map(target => ({ target, value: value || '' }))
+ },
Editor: ({ config, onChange }) => {
const handleAddOption = () => {
onChange({
diff --git a/src/component/BasicElements/TextareaElement.tsx b/src/component/BasicElements/TextareaElement.tsx
index f1da0b0..4aef047 100644
--- a/src/component/BasicElements/TextareaElement.tsx
+++ b/src/component/BasicElements/TextareaElement.tsx
@@ -19,6 +19,10 @@ export const textareaDefinition: ElementDefinition = {
targetCells: [],
},
mapToCells: config => config.targetCells || [],
+ mapToCellValues: (config, value) => {
+ const cells = config.targetCells || []
+ return cells.map(target => ({ target, value: value || '' }))
+ },
Editor: () => (
Дополнительные настройки отсутствуют
diff --git a/src/component/DualSpreadsheet/DualSpreadsheet.tsx b/src/component/DualSpreadsheet/DualSpreadsheet.tsx
index 9cca5ec..18c0163 100644
--- a/src/component/DualSpreadsheet/DualSpreadsheet.tsx
+++ b/src/component/DualSpreadsheet/DualSpreadsheet.tsx
@@ -1,6 +1,6 @@
+import { useFileData, useLatestFileForTemplate } from '@/hook/useFileQueries'
import { useMultiSheetEngine } from '@/hook/useMultiSheetEngine'
import { useSheetAutoSave } from '@/hook/useSheetAutoSave'
-import { getFileData, getLatestFileForTemplate } from '@/service/fileApiService'
import { produce } from 'immer'
import React, {
FC,
@@ -78,6 +78,10 @@ const DualSpreadsheet: FC
= ({
row: number
col: number
} | null>(null)
+
+ // Флаг, что пользователь изменил хотя бы одну ячейку
+ const [userEdited, setUserEdited] = useState(false)
+
// Состояние для множественного выделения
const [multiSelection, setMultiSelection] = useState([])
const [isDragging, setIsDragging] = useState(false)
@@ -95,7 +99,14 @@ const DualSpreadsheet: FC = ({
>({}) // Базовые данные файла
const [highlightedCells, setHighlightedCells] = useState(
[]
- ) // Подсвеченные ячейки для формул
+ )
+
+ // React Query хуки для оптимизации API запросов
+ const { data: latestFile, isLoading: isLoadingLatestFile } =
+ useLatestFileForTemplate(templateId)
+ const { data: fileData, isLoading: isLoadingFileData } = useFileData(
+ latestFile?.id
+ )
// Мемоизируем начальное состояние ширин колонок
const initialColumnWidths = useMemo(
@@ -109,46 +120,22 @@ const DualSpreadsheet: FC = ({
const [columnWidths, setColumnWidths] =
useState>(initialColumnWidths)
- // Мемоизируем начальные refs объекты
- const initialGridRefs = useMemo(
- () => ({
- report: null,
- calculations: null,
- }),
- []
- )
-
- const initialContainerRefs = useMemo(
- () => ({
- report: null,
- calculations: null,
- }),
- []
- )
-
- const initialHeaderRefs = useMemo(
- () => ({
- report: null,
- calculations: null,
- }),
- []
- )
-
- const initialRowHeaderRefs = useMemo(
- () => ({
- report: null,
- calculations: null,
- }),
- []
- )
-
- const gridRefs = useRef>(initialGridRefs)
- const containerRefs =
- useRef>(initialContainerRefs)
- const headerRefs =
- useRef>(initialHeaderRefs)
- const rowHeaderRefs =
- useRef>(initialRowHeaderRefs)
+ const gridRefs = useRef>({
+ report: null,
+ calculations: null,
+ })
+ const containerRefs = useRef>({
+ report: null,
+ calculations: null,
+ })
+ const headerRefs = useRef>({
+ report: null,
+ calculations: null,
+ })
+ const rowHeaderRefs = useRef>({
+ report: null,
+ calculations: null,
+ })
const activeCellInputRef = useRef(null)
const formulaBarRef = useRef(null)
@@ -171,22 +158,12 @@ const DualSpreadsheet: FC = ({
initialTemplateValues.current[cellAddress] = String(value || '')
})
})
- // console.log(
- // '✅ Загружено значений baseFileData для сравнения (приоритет):',
- // Object.keys(initialTemplateValues.current).length
- //)
} else if (hasTemplateData) {
Object.entries(templateData).forEach(([sheetName, sheetData]) => {
Object.entries(sheetData).forEach(([cellAddress, value]) => {
initialTemplateValues.current[cellAddress] = String(value || '')
})
})
- // console.log(
- // '✅ Загружено значений templateData для сравнения (fallback):',
- // Object.keys(initialTemplateValues.current).length
- // )
- } else {
- // console.log('⚠️ DualSpreadsheet: Нет базовых данных для сравнения')
}
}, [baseFileData, templateData])
@@ -212,7 +189,7 @@ const DualSpreadsheet: FC = ({
const getCachedCellData = useCallback(
(sheetType: SheetType): Record => {
- const cacheKey = `${sheetType}-${revision}-${JSON.stringify(columnWidths[sheetType])}`
+ const cacheKey = `${sheetType}-${revision}-${columnWidths[sheetType].join('-')}`
if (cellDataCache.current[cacheKey]) {
return cellDataCache.current[cacheKey]
@@ -262,19 +239,6 @@ const DualSpreadsheet: FC = ({
const cellName = getColumnLabel(col) + (row + 1)
const baseFileValue = initialTemplateValues.current[cellName] ?? '' // Базовое значение из файла
- // Отладка только для измененных ячеек
- if (
- sheetType === 'report' &&
- baseFileValue &&
- rawValue !== baseFileValue
- ) {
- // console.log(`🔧 Измененная ячейка ${cellName}:`, {
- // baseFileValue,
- // rawValue,
- // isModified: true,
- // })
- }
-
// Нормализуем значения для сравнения
const normalizeValue = (value: any): string => {
if (value === null || value === undefined) return ''
@@ -290,15 +254,6 @@ const DualSpreadsheet: FC = ({
const isModified =
sheetType === 'report' && normalizedRaw !== normalizedBaseFile
- // Отладка только для измененных ячеек
- if (sheetType === 'report' && isModified) {
- // console.log(`🔍 Измененная ячейка ${cellName}:`, {
- // baseFileValue,
- // rawValue,
- // isModified,
- // })
- }
-
cachedData[cellKey] = {
displayValue,
rawValue,
@@ -343,11 +298,6 @@ const DualSpreadsheet: FC = ({
)
setHighlightedCells(highlights)
-
- // console.log('🎨 Подсветка ячеек для формулы:', formula, {
- // references,
- // highlights,
- // })
} catch (error) {
console.error('Ошибка при обновлении подсветки ячеек:', error)
setHighlightedCells([])
@@ -422,23 +372,6 @@ const DualSpreadsheet: FC = ({
(save: boolean) => {
if (save && selectedCell) {
const sheetName = SHEET_NAMES[activeSheet]
- const cellName =
- getColumnLabel(selectedCell.col) + (selectedCell.row + 1)
- const oldValue = multiSheetEngine.getCellRawValue(
- sheetName,
- selectedCell.row,
- selectedCell.col
- )
- const baseFileValue = initialTemplateValues.current[cellName] ?? '' // Базовое значение из файла
-
- // console.log('💾 Сохранение ячейки:', {
- //cellName,
- //oldValue,
- //newValue: editingValue,
- //baseFileValue,
- //isChanged: oldValue !== editingValue,
- //isModifiedFromBaseFile: baseFileValue !== editingValue,
- //})
multiSheetEngine.setCellValueWithoutRecalc(
sheetName,
@@ -461,23 +394,25 @@ const DualSpreadsheet: FC = ({
(newValue: string) => {
if (isEditing) {
setEditingValue(newValue)
+ if (!userEdited) setUserEdited(true)
}
},
- [isEditing]
+ [isEditing, userEdited]
)
const handleClearCell = useCallback(() => {
if (!selectedCell) return
const sheetName = SHEET_NAMES[activeSheet]
+
multiSheetEngine.setCellValueWithoutRecalc(
sheetName,
selectedCell.row,
selectedCell.col,
''
)
+ setUserEdited(true)
updateRevision()
multiSheetEngine.debouncedRecalc()
- // Сбрасываем множественное выделение при очистке одиночной ячейки
setMultiSelection([])
}, [activeSheet, selectedCell, multiSheetEngine, updateRevision])
@@ -515,22 +450,19 @@ const DualSpreadsheet: FC = ({
const sheetName = SHEET_NAMES[activeSheet]
const cellsToDelete = getAllCellsInSelections(multiSelection)
- cellsToDelete.forEach(({ row, col }: { row: number; col: number }) => {
+ cellsToDelete.forEach(({ row, col }) => {
multiSheetEngine.setCellValueWithoutRecalc(sheetName, row, col, '')
})
+ setUserEdited(true)
updateRevision()
multiSheetEngine.debouncedRecalc()
-
- // Очищаем выделение после удаления
setMultiSelection([])
}, [multiSelection, activeSheet, multiSheetEngine, updateRevision])
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
- if (isEditing) {
- return
- }
+ if (isEditing) return
if (!selectedCell && e.key.match(/^(Arrow|Enter|F2|Delete|Backspace)$/)) {
moveSelection(0, 0)
@@ -595,6 +527,7 @@ const DualSpreadsheet: FC = ({
setEditingSource('cell')
setEditingValue(e.key)
setIsEditing(true)
+ if (!userEdited) setUserEdited(true)
}
},
[
@@ -610,13 +543,6 @@ const DualSpreadsheet: FC = ({
const handleCellClick = useCallback(
(row: number, col: number, event?: React.MouseEvent) => {
- // console.log('🖱️ Обычный клик по ячейке:', {
- // row,
- //col,
- //isEditing,
- //activeSheet,
- //})
-
if (isEditing) {
stopEditing(true)
}
@@ -710,44 +636,23 @@ const DualSpreadsheet: FC = ({
// Новый обработчик для кликов в режиме редактирования - вставляет ссылку
const handleCellReferenceClick = useCallback(
(targetSheetType: SheetType, row: number, col: number) => {
- // console.log('🔗 Клик для вставки ссылки:', {
- //targetSheetType,
- //row,
- //col,
- //isEditing,
- //selectedCell,
- //editingValue,
- //})
-
- // Проверяем, что мы в режиме редактирования
- if (!isEditing || !selectedCell) {
- // console.log(
- // '⚠️ Игнорируем клик ссылки: не в режиме редактирования или нет выбранной ячейки'
- //)
+ if (!isEditing || !selectedCell || !editingValue.startsWith('=')) {
+ stopEditing(true)
+ setSelectedCell({ row, col })
return
}
- // Создаем ссылку на ячейку
let cellReference: string
- // Если это другой лист, делаем квалифицированную ссылку
if (targetSheetType !== activeSheet) {
cellReference = createQualifiedCellReference(targetSheetType, row, col)
- // console.log('📋 Создана межлистовая ссылка:', cellReference)
} else {
- // Если это тот же лист, делаем локальную ссылку
cellReference = createCellAddress(row, col)
- // console.log('📋 Создана локальная ссылка:', cellReference)
}
- // Добавляем ссылку к текущему значению редактирования
const newValue = editingValue + cellReference
- // console.log('✏️ Новое значение формулы:', {
- //oldValue: editingValue,
- //newValue,
- //})
-
setEditingValue(newValue)
+ if (!userEdited) setUserEdited(true)
if (editingSource === 'formula') {
if (formulaBarRef.current) {
@@ -795,13 +700,12 @@ const DualSpreadsheet: FC = ({
const handleFormulaBarChange = useCallback(
(newValue: string) => {
if (selectedCell) {
- if (!isEditing) {
- setIsEditing(true)
- }
+ if (!isEditing) setIsEditing(true)
setEditingValue(newValue)
+ if (!userEdited) setUserEdited(true)
}
},
- [selectedCell, isEditing]
+ [selectedCell, isEditing, userEdited]
)
const handleFormulaBarFocus = useCallback(() => {
@@ -860,32 +764,25 @@ const DualSpreadsheet: FC = ({
)
// Мемоизируем состояние ширин таблиц
- const initialSpreadsheetWidths = useMemo(
- () => ({
- L: 0.5,
- R: 0.5,
- }),
- []
- )
-
- const [spreadsheetWidths, setSpreadsheetWidths] = useState(
- initialSpreadsheetWidths
- )
+ const [spreadsheetWidths, setSpreadsheetWidths] = useState({
+ L: 0.5,
+ R: 0.5,
+ })
// Мемоизируем опции автосохранения
const autoSaveOptions = useMemo(
() => ({
templateId,
autoSaveDelay: 2000,
- enableAutoSave,
+ enableAutoSave: enableAutoSave && userEdited,
}),
- [templateId, enableAutoSave]
+ [templateId, enableAutoSave, userEdited]
)
// Мемоизируем getAllModifiedCells для предотвращения частых ререндеров хука автосохранения
const memoizedGetAllModifiedCells = useCallback(
multiSheetEngine.getAllModifiedCells,
- [multiSheetEngine.getAllModifiedCells]
+ [revision] // Используем revision вместо функции для более стабильной мемоизации
)
// Используем новый хук автосохранения
@@ -896,8 +793,7 @@ const DualSpreadsheet: FC = ({
lastSaveError,
manualSave,
clearError,
- loadSavedSheets,
- loadSheets, // Добавляем асинхронную функцию загрузки
+ loadSheets,
} = useSheetAutoSave(
memoizedGetAllModifiedCells,
multiSheetEngine.isCalculating,
@@ -924,33 +820,12 @@ const DualSpreadsheet: FC = ({
let finalBaseData: Record> = {}
- // 1. ЗАГРУЖАЕМ БАЗОВЫЕ ДАННЫЕ ФАЙЛА
- if (templateId) {
- try {
- const latestFile = await getLatestFileForTemplate(templateId)
- if (latestFile) {
- // console.log('📁 Найден последний файл для шаблона:', latestFile.name)
- const fileData = await getFileData(latestFile.id)
-
- // TODO: Пока getFileData возвращает пустые данные, используем templateData
- // Когда API будет готов, здесь будут реальные данные файла
- if (Object.keys(fileData.data).length > 0) {
- finalBaseData = { [SHEET_NAMES.report]: fileData.data }
- setBaseFileData(finalBaseData)
- // Сохраняем baseline, чтобы в L попадали только пользовательские изменения
- setTemplateData(finalBaseData)
- // console.log('📄 Загружены базовые данные файла:', finalBaseData)
- } else {
- // console.log(
- // '⚠️ getFileData вернул пустые данные, используем templateData'
- //)
- }
- } else {
- // console.log('⚠️ Файл не найден для templateId:', templateId)
- }
- } catch (error) {
- console.error('❌ Ошибка при загрузке данных файла:', error)
- }
+ // 1. ИСПОЛЬЗУЕМ ДАННЫЕ ИЗ REACT QUERY КЕША
+ if (fileData && Object.keys(fileData.data).length > 0) {
+ finalBaseData = { [SHEET_NAMES.report]: fileData.data }
+ setBaseFileData(finalBaseData)
+ // Сохраняем baseline, чтобы в листы попадали только пользовательские изменения
+ setTemplateData(finalBaseData)
}
// 2. ИСПОЛЬЗУЕМ templateData КАК FALLBACK или основные данные
@@ -961,16 +836,11 @@ const DualSpreadsheet: FC = ({
if (hasTemplateData && Object.keys(finalBaseData).length === 0) {
finalBaseData = templateData
setBaseFileData(finalBaseData)
- // console.log(
- // '📄 Используем templateData как базовые данные:',
- // finalBaseData
- //)
setTemplateData(templateData)
}
// 3. ЗАГРУЖАЕМ ПОЛЬЗОВАТЕЛЬСКИЕ ИЗМЕНЕНИЯ ИЗ API
try {
- // console.log('🔍 Загружаем пользовательские изменения из API...')
const apiSheets = await loadSheets()
// Преобразуем листы API в формат данных
@@ -995,17 +865,8 @@ const DualSpreadsheet: FC = ({
}
})
- // console.log(
- // '📂 Загружены пользовательские изменения:',
- // Object.keys(userChanges)
- //)
-
// 4. ОБЪЕДИНЯЕМ БАЗОВЫЕ ДАННЫЕ С ПОЛЬЗОВАТЕЛЬСКИМИ ИЗМЕНЕНИЯМИ
if (Object.keys(userChanges).length > 0) {
- // console.log(
- // '🔄 Объединяем базовые данные с пользовательскими изменениями'
- //)
-
// Создаем объединенные данные: базовые данные файла + пользовательские изменения
const mergedData: Record> = {}
@@ -1024,24 +885,15 @@ const DualSpreadsheet: FC = ({
})
})
- // console.log(
- // '📊 Загружаем объединенные данные (базовые + пользовательские)'
- //)
multiSheetEngine.loadData(mergedData)
} else if (Object.keys(finalBaseData).length > 0) {
- // console.log(
- // '📋 Используем только базовые данные (нет пользовательских изменений)'
- //)
multiSheetEngine.loadData(finalBaseData)
- } else {
- // console.log('⚠️ Нет данных для загрузки')
}
} catch (error) {
console.error('❌ Ошибка при загрузке пользовательских изменений:', error)
// В случае ошибки используем только базовые данные
if (Object.keys(finalBaseData).length > 0) {
- // console.log('📋 Ошибка API, используем только базовые данные')
multiSheetEngine.loadData(finalBaseData)
}
}
@@ -1050,6 +902,7 @@ const DualSpreadsheet: FC = ({
}, [
templateId,
templateData,
+ fileData,
loadSheets,
multiSheetEngine,
setTemplateData,
@@ -1057,9 +910,22 @@ const DualSpreadsheet: FC = ({
])
// Загружаем сохраненные формулы при инициализации
+ // Ждем загрузки данных из React Query перед инициализацией
useEffect(() => {
- initializeData()
- }, [initializeData])
+ // Инициализируем только когда все данные загружены или их нет вообще
+ const canInitialize =
+ !isLoadingLatestFile && !isLoadingFileData && templateId && !isInitialized
+
+ if (canInitialize) {
+ initializeData()
+ }
+ }, [
+ initializeData,
+ isLoadingLatestFile,
+ isLoadingFileData,
+ templateId,
+ isInitialized,
+ ])
return (
diff --git a/src/component/StandardsElement/StandardsEditor.tsx b/src/component/StandardsElement/StandardsEditor.tsx
index b6b8894..b5abd44 100644
--- a/src/component/StandardsElement/StandardsEditor.tsx
+++ b/src/component/StandardsElement/StandardsEditor.tsx
@@ -1,5 +1,3 @@
-import { Button } from '@/component/ui/button'
-import { Input } from '@/component/ui/input'
import {
Select,
SelectContent,
@@ -8,10 +6,9 @@ import {
SelectValue,
} from '@/component/ui/select'
import { CellTarget } from '@/type/template'
-import { Calendar, Settings, Trash2 } from 'lucide-react'
+import { Info, Settings } from 'lucide-react'
interface StandardsConfig {
- registryNumber: string // ГРСИ
sheet: string // Лист Excel
startCell: string // Первая ячейка (например "A10")
maxItems: number // Обычно 7
@@ -23,119 +20,13 @@ interface StandardsEditorProps {
onChange: (config: StandardsConfig) => void
}
-const CellTargetEditor: React.FC<{
- targets: CellTarget[]
- onChange: (targets: CellTarget[]) => void
-}> = ({ targets, onChange }) => {
- const handleAddTarget = () => {
- onChange([...targets, { sheet: 'L', cell: '', displayName: '' }])
- }
-
- const handleUpdateTarget = (
- index: number,
- field: keyof CellTarget,
- value: string
- ) => {
- const updatedTargets = targets.map((target, i) =>
- i === index ? { ...target, [field]: value } : target
- )
- onChange(updatedTargets)
- }
-
- const handleRemoveTarget = (index: number) => {
- onChange(targets.filter((_, i) => i !== index))
- }
-
- return (
-
-
- Целевые ячейки
-
-
- Добавить ячейку
-
-
-
- {targets.map((target, index) => (
-
-
-
-
- Лист
-
-
- handleUpdateTarget(index, 'sheet', value)
- }
- >
-
-
-
-
- L (Левый)
- R (Правый)
- Report
- Calculations
-
-
-
-
-
- Ячейка
-
-
- handleUpdateTarget(index, 'cell', e.target.value)
- }
- className="h-8"
- />
-
-
-
- Название
-
-
- handleUpdateTarget(index, 'displayName', e.target.value)
- }
- className="h-8"
- />
-
-
-
handleRemoveTarget(index)}
- className="h-6 text-xs text-red-600 hover:text-red-700"
- >
-
- Удалить
-
-
- ))}
- {targets.length === 0 && (
-
- Добавьте ячейки для связи с элементом
-
- )}
-
-
- )
-}
-
export const StandardsEditor: React.FC
= ({
config,
onChange,
}) => {
// Обеспечиваем значения по умолчанию
const safeConfig = {
- registryNumber: config.registryNumber || '',
- sheet: config.sheet || 'Report',
+ sheet: config.sheet || 'L',
startCell: config.startCell || 'A1',
maxItems: config.maxItems || 7,
targetCells: config.targetCells || [],
@@ -157,86 +48,36 @@ export const StandardsEditor: React.FC = ({
-
-
- Номер ГРСИ
- updateConfig({ registryNumber: e.target.value })}
- />
-
-
-
- Максимум эталонов
- updateConfig({ maxItems: parseInt(value) })}
- >
-
-
-
-
- 3 эталона
- 5 эталонов
- 7 эталонов
- 10 эталонов
-
-
-
-
-
-
-
- Лист Excel
- updateConfig({ sheet: value })}
- >
-
-
-
-
- L (Левый)
- R (Правый)
- Report
- Calculations
-
-
-
-
-
- Начальная ячейка
- updateConfig({ startCell: e.target.value })}
- />
-
-
-
-
Целевые ячейки
+
Максимум эталонов
+
updateConfig({ maxItems: parseInt(value) })}
+ >
+
+
+
+
+ 3 эталона
+ 5 эталонов
+ 7 эталонов
+ 10 эталонов
+
+
- Ячейки для записи выбранных эталонов (автоматически генерируются)
+ Максимальное количество эталонов для выбора
-
updateConfig({ targetCells: targets })}
- />
-
+
-
- Автоматическое заполнение
-
+
Настройка ячеек
- При выборе эталонов данные будут записаны в указанные ячейки. Если
- выбрано меньше эталонов, чем максимальное количество, оставшиеся
- ячейки останутся пустыми.
+ Ячейки для размещения эталонов настраиваются в разделе "Целевые
+ ячейки" выше. Эталоны будут записываться в указанные ячейки по
+ порядку.
diff --git a/src/component/StandardsElement/StandardsPreview.tsx b/src/component/StandardsElement/StandardsPreview.tsx
index 31e0e07..22484b0 100644
--- a/src/component/StandardsElement/StandardsPreview.tsx
+++ b/src/component/StandardsElement/StandardsPreview.tsx
@@ -1,9 +1,9 @@
import { Badge } from '@/component/ui/badge'
import { Button } from '@/component/ui/button'
-import { Calendar, Settings } from 'lucide-react'
+import { Label } from '@/component/ui/label'
+import { Settings, Wrench } from 'lucide-react'
interface StandardsConfig {
- registryNumber: string
sheet: string
startCell: string
maxItems: number
@@ -14,51 +14,122 @@ interface StandardsPreviewProps {
config: StandardsConfig
}
+// Моковые данные для эталонов (такие же как в definition.tsx)
+const mockStandards = [
+ {
+ id: '1',
+ name: 'Эталон массы 1 кг',
+ shortName: 'ЭМ-1кг',
+ type: 'Масса',
+ registryNumber: 'ГРСИ 12345-01',
+ range: '0.5-2 кг',
+ accuracy: '±0.001 г',
+ certificateNumber: 'СИ-2024-001',
+ validUntil: '2025-12-31',
+ },
+ {
+ id: '2',
+ name: 'Эталон длины 1 м',
+ shortName: 'ЭД-1м',
+ type: 'Длина',
+ registryNumber: 'ГРСИ 12345-02',
+ range: '0.5-2 м',
+ accuracy: '±0.001 мм',
+ certificateNumber: 'СИ-2024-002',
+ validUntil: '2025-06-30',
+ },
+ {
+ id: '3',
+ name: 'Эталон температуры 20°C',
+ shortName: 'ЭТ-20°C',
+ type: 'Температура',
+ registryNumber: 'ГРСИ 12345-03',
+ range: '15-25°C',
+ accuracy: '±0.01°C',
+ certificateNumber: 'СИ-2024-003',
+ validUntil: '2025-03-15',
+ },
+ {
+ id: '4',
+ name: 'Эталон давления 1 Па',
+ shortName: 'ЭД-1Па',
+ type: 'Давление',
+ registryNumber: 'ГРСИ 12345-04',
+ range: '0.5-2 Па',
+ accuracy: '±0.001 Па',
+ certificateNumber: 'СИ-2024-004',
+ validUntil: '2025-09-20',
+ },
+ {
+ id: '5',
+ name: 'Эталон времени 1 с',
+ shortName: 'ЭВ-1с',
+ type: 'Время',
+ registryNumber: 'ГРСИ 12345-05',
+ range: '0.5-2 с',
+ accuracy: '±0.000001 с',
+ certificateNumber: 'СИ-2024-005',
+ validUntil: '2025-12-01',
+ },
+]
+
+// Функция для получения типа эталона в сокращенном виде
+const getStandardTypeBadge = (type: string) => {
+ switch (type) {
+ case 'Манометр грузопоршневой':
+ return 'МГП'
+ case 'Калибратор давления':
+ return 'КД'
+ default:
+ return 'МО'
+ }
+}
+
export const StandardsPreview: React.FC
= ({
config,
}) => {
+ // Берем первые config.maxItems эталонов для предпросмотра
+ const previewStandards = mockStandards.slice(0, config.maxItems || 7)
+
return (
-
-
Предпросмотр
-
-
-
-
- Измерительные эталоны
-
+
+ {/* Блок с измерительными эталонами */}
+
+
+ Эталоны
+
+
+ Настроить
+
+
-
-
- ГРСИ: {config.registryNumber || 'Не указан'}
-
-
- Максимум: {config.maxItems} эталонов
-
+
+
+ {previewStandards.map((standard, index) => (
+
+
+
+ {index + 1}
+
+
+
+
+ {standard.shortName}
+
+
+ {standard.accuracy} • {standard.range}
+
+
+
+
+ {getStandardTypeBadge(standard.type)}
+
+
+ ))}
-
-
-
-
- Выбрать эталоны
-
-
- 0/{config.maxItems} выбрано
-
-
-
- {config.targetCells && config.targetCells.length > 0 && (
-
- {config.targetCells.map((target, index) => (
-
- {target.sheet}!{target.cell}
-
- ))}
-
- )}
diff --git a/src/component/StandardsElement/definition.tsx b/src/component/StandardsElement/definition.tsx
index b0de741..5403654 100644
--- a/src/component/StandardsElement/definition.tsx
+++ b/src/component/StandardsElement/definition.tsx
@@ -1,12 +1,91 @@
+import { Badge } from '@/component/ui/badge'
+import { Button } from '@/component/ui/button'
+import { Label } from '@/component/ui/label'
import { ElementDefinition } from '@/entitiy/element/model/interface'
-import { parseCellAddress } from '@/lib/cell-utils'
import { CellTarget } from '@/type/template'
-import { Weight } from 'lucide-react'
+import { Settings, Weight, Wrench } from 'lucide-react'
+import { useEffect, useState } from 'react'
import { StandardsEditor } from './StandardsEditor'
import { StandardsPreview } from './StandardsPreview'
+import { StandardsSelector } from './StandardsSelector'
+
+// Моковые данные для эталонов (такие же как в StandardsSelector)
+const mockStandards = [
+ {
+ id: '1',
+ name: 'Эталон массы 1 кг',
+ shortName: 'ЭМ-1кг',
+ type: 'Масса',
+ registryNumber: 'ГРСИ 12345-01',
+ range: '0.5-2 кг',
+ accuracy: '±0.001 г',
+ certificateNumber: 'СИ-2024-001',
+ validUntil: '2025-12-31',
+ },
+ {
+ id: '2',
+ name: 'Эталон длины 1 м',
+ shortName: 'ЭД-1м',
+ type: 'Длина',
+ registryNumber: 'ГРСИ 12345-02',
+ range: '0.5-2 м',
+ accuracy: '±0.001 мм',
+ certificateNumber: 'СИ-2024-002',
+ validUntil: '2025-06-30',
+ },
+ {
+ id: '3',
+ name: 'Эталон температуры 20°C',
+ shortName: 'ЭТ-20°C',
+ type: 'Температура',
+ registryNumber: 'ГРСИ 12345-03',
+ range: '15-25°C',
+ accuracy: '±0.01°C',
+ certificateNumber: 'СИ-2024-003',
+ validUntil: '2025-03-15',
+ },
+ {
+ id: '4',
+ name: 'Эталон давления 1 Па',
+ shortName: 'ЭД-1Па',
+ type: 'Давление',
+ registryNumber: 'ГРСИ 12345-04',
+ range: '0.5-2 Па',
+ accuracy: '±0.001 Па',
+ certificateNumber: 'СИ-2024-004',
+ validUntil: '2025-09-20',
+ },
+ {
+ id: '5',
+ name: 'Эталон времени 1 с',
+ shortName: 'ЭВ-1с',
+ type: 'Время',
+ registryNumber: 'ГРСИ 12345-05',
+ range: '0.5-2 с',
+ accuracy: '±0.000001 с',
+ certificateNumber: 'СИ-2024-005',
+ validUntil: '2025-12-01',
+ },
+]
+
+// Функция для получения данных эталона по ID
+const getStandardById = (id: string) => {
+ return mockStandards.find(standard => standard.id === id)
+}
+
+// Функция для получения типа эталона в сокращенном виде
+const getStandardTypeBadge = (type: string) => {
+ switch (type) {
+ case 'Манометр грузопоршневой':
+ return 'МГП'
+ case 'Калибратор давления':
+ return 'КД'
+ default:
+ return 'МО'
+ }
+}
interface StandardsConfig {
- registryNumber: string // ГРСИ
sheet: string // Лист Excel
startCell: string // Первая ячейка (например "A10")
maxItems: number // Обычно 7
@@ -20,39 +99,125 @@ export const standardsDefinition: ElementDefinition
=
icon: ,
version: 1,
defaultConfig: {
- registryNumber: '',
- sheet: 'Report',
+ sheet: 'L',
startCell: 'A1',
maxItems: 7,
targetCells: [],
},
Editor: StandardsEditor,
Preview: StandardsPreview,
- Render: () => {
- // Здесь нужно создать обертку для StandardsDialog
- // Пока возвращаем null, так как StandardsDialog требует дополнительные пропсы
- return null
+ Render: ({ config, value, onChange }) => {
+ const [isStandardsDialogOpen, setIsStandardsDialogOpen] = useState(false)
+ const [selectedStandardIds, setSelectedStandardIds] = useState(
+ []
+ )
+
+ // Если value содержит названия эталонов, находим соответствующие ID
+ useEffect(() => {
+ if (Array.isArray(value) && value.length > 0) {
+ const ids = value
+ .map(name => {
+ const standard = mockStandards.find(s => s.name === name)
+ return standard?.id || ''
+ })
+ .filter(Boolean)
+ setSelectedStandardIds(ids)
+ }
+ }, [value])
+
+ const selectedStandardsData = selectedStandardIds
+ .map(id => getStandardById(id))
+ .filter(Boolean)
+
+ return (
+
+ {/* Блок с измерительными эталонами */}
+
+
+ Эталоны
+ setIsStandardsDialogOpen(true)}
+ className="h-7 px-2"
+ >
+
+ Настроить
+
+
+
+
+ {selectedStandardsData.length > 0 ? (
+
+ {selectedStandardsData.map(
+ (standard, index) =>
+ standard && (
+
+
+
+ {index + 1}
+
+
+
+
+ {standard.shortName}
+
+
+ {standard.accuracy} • {standard.range}
+
+
+
+
+ {getStandardTypeBadge(standard.type)}
+
+
+ )
+ )}
+
+ ) : (
+
+
+ Эталоны не выбраны
+
+ )}
+
+
+
+
setIsStandardsDialogOpen(false)}
+ value={selectedStandardIds}
+ onChange={standardIds => {
+ setSelectedStandardIds(standardIds)
+ // Преобразуем ID эталонов в их названия для сохранения в ячейки
+ const standardNames = standardIds.map(
+ id => getStandardById(id)?.name || ''
+ )
+ onChange?.(standardNames)
+ }}
+ />
+
+ )
},
mapToCells: cfg => {
- const res: CellTarget[] = []
- const parsed = parseCellAddress(cfg.startCell)
+ // Используем только ячейки, указанные пользователем в targetCells
+ return cfg.targetCells || []
+ },
+ mapToCellValues: (cfg, value) => {
+ // Используем targetCells из конфигурации
+ const cells = cfg.targetCells || []
- if (!parsed) {
- // Если не удалось распарсить, возвращаем пустой массив
- return res
- }
-
- const { column, row } = parsed
-
- // Генерируем ячейки вертикально (A1, A2, A3...)
- for (let i = 0; i < cfg.maxItems; i++) {
- res.push({
- sheet: cfg.sheet,
- cell: `${column}${row + i}`,
- displayName: `Эталон ${i + 1}`,
- })
- }
-
- return res
+ // Сопоставляем значения с ячейками
+ const standardNames = Array.isArray(value) ? value : []
+ return cells.map((target, idx) => ({
+ target,
+ value: standardNames[idx] || '', // Берем значение по индексу или пустую строку
+ }))
},
}
diff --git a/src/component/TemplateManager/ElementConstructor.tsx b/src/component/TemplateManager/ElementConstructor.tsx
index f24cef8..2b49660 100644
--- a/src/component/TemplateManager/ElementConstructor.tsx
+++ b/src/component/TemplateManager/ElementConstructor.tsx
@@ -2,6 +2,7 @@ import {
getAvailableElementTypes,
getElementDefinition,
} from '@/entitiy/element/model/interface'
+import { useToast } from '@/lib/hooks/useToast'
import {
autoArrangeElements,
findFreePosition,
@@ -14,17 +15,7 @@ import {
Template,
TemplateElement,
} from '@/type/template'
-import {
- Calendar,
- Droplets,
- Edit3,
- Gauge,
- Plus,
- Radio,
- Thermometer,
- Trash2,
- Zap,
-} from 'lucide-react'
+import { Plus, Trash2 } from 'lucide-react'
import React, { useState } from 'react'
import { Button } from '../ui/button'
import { Checkbox } from '../ui/checkbox'
@@ -43,7 +34,6 @@ import {
SelectTrigger,
SelectValue,
} from '../ui/select'
-import { Textarea } from '../ui/textarea'
import { ElementFormulaBar } from './ElementFormulaBar'
import { VisualLayoutEditor } from './VisualLayoutEditor'
@@ -56,6 +46,8 @@ const CellTargetEditor: React.FC<{
targets: CellTarget[]
onChange: (targets: CellTarget[]) => void
}> = ({ targets, onChange }) => {
+ const { warning } = useToast()
+
const handleAddTarget = () => {
onChange([...targets, { sheet: 'L', cell: '' }])
}
@@ -75,6 +67,21 @@ const CellTargetEditor: React.FC<{
onChange(targets.filter((_, i) => i !== index))
}
+ const handleCellInputChange = (index: number, value: string) => {
+ // Проверяем на наличие кириллицы
+ const hasCyrillic = /[а-яё]/i.test(value)
+
+ if (hasCyrillic) {
+ warning('Кириллические символы не разрешены в адресе ячейки', {
+ title: 'Недопустимый символ',
+ duration: 3000,
+ })
+ return // Не обновляем значение
+ }
+
+ handleUpdateTarget(index, 'cell', value)
+ }
+
return (
@@ -119,9 +126,7 @@ const CellTargetEditor: React.FC<{
- handleUpdateTarget(index, 'cell', e.target.value)
- }
+ onChange={e => handleCellInputChange(index, e.target.value)}
className="h-7 text-xs"
/>
@@ -159,163 +164,7 @@ const ElementPreview: React.FC<{ element: Partial
}> = ({
return
}
- // Fallback для старых элементов
- const renderPreviewInput = () => {
- switch (element.type) {
- case 'text':
- return (
-
- )
-
- case 'textarea':
- return (
-
- )
-
- case 'number':
- return (
-
- )
-
- case 'date':
- return
-
- case 'select':
- return (
-
-
-
- {element.placeholder || 'Выберите значение'}
-
-
-
- )
-
- case 'radio':
- return (
-
- {element.options?.length ? (
- element.options.map((option, index) => (
-
- ))
- ) : (
-
- )}
-
- )
-
- case 'checkbox':
- return (
-
-
-
- {element.placeholder || element.label || 'Отметить'}
-
-
- )
-
- case 'standards':
- return (
-
-
-
- {element.placeholder || 'Выберите эталоны'}
-
-
-
- Выбрать
-
-
-
- Выбрано эталонов: 0
-
-
- )
-
- case 'calibration_conditions':
- return (
-
-
-
-
-
- 20°C
-
-
-
- 65%
-
-
-
- 101.3кПа
-
-
-
- 220В
-
-
-
- 50Гц
-
-
-
-
-
-
-
- )
-
- case 'button_group':
- return (
-
-
- {element.options?.length ? (
- element.options.map((option, index) => (
-
- {option.label || `Кнопка ${index + 1}`}
-
- ))
- ) : (
-
- {element.placeholder || 'Добавьте кнопки'}
-
- )}
-
-
- )
-
- default:
- return (
-
- Выберите тип элемента
-
- )
- }
- }
-
+ // Fallback для неизвестных типов элементов
return (
Предпросмотр
@@ -326,7 +175,13 @@ const ElementPreview: React.FC<{ element: Partial
}> = ({
{element.required && * }
)}
- {renderPreviewInput()}
+
+
+ {element.type
+ ? `Элемент типа "${element.type}" не найден`
+ : 'Выберите тип элемента'}
+
+
{element.targetCells && element.targetCells.length > 0 && (
{element.targetCells.map((target, index) => (
@@ -699,7 +554,6 @@ export const ElementConstructor: React.FC
= ({
showGrid: true,
}
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
- const [isSettingsOpen, setIsSettingsOpen] = useState(false)
const [editingElement, setEditingElement] = useState(
null
)
diff --git a/src/component/ui/toast.tsx b/src/component/ui/toast.tsx
index 5a62fe6..fe57c82 100644
--- a/src/component/ui/toast.tsx
+++ b/src/component/ui/toast.tsx
@@ -165,7 +165,7 @@ export const ToastContainer: FC = ({
if (toasts.length === 0) return null
return (
-
+
{toasts.map(toast => (
diff --git a/src/entitiy/element/model/implementations/ButtonGroup.tsx b/src/entitiy/element/model/implementations/ButtonGroup.tsx
index cb8f872..e3a3854 100644
--- a/src/entitiy/element/model/implementations/ButtonGroup.tsx
+++ b/src/entitiy/element/model/implementations/ButtonGroup.tsx
@@ -33,6 +33,15 @@ export const buttonGroupDefinition: ElementDefinition<
},
// Берём целевые ячейки прямо из конфига, если они заданы в редакторе
mapToCells: cfg => cfg.targetCells || [],
+ mapToCellValues: (config, value) => {
+ const cells = config.targetCells || []
+
+ // Находим option по value чтобы получить label
+ const selectedOption = config.options?.find(opt => opt.value === value)
+ const cellValue = selectedOption ? selectedOption.label : value || ''
+
+ return cells.map(target => ({ target, value: cellValue }))
+ },
Editor: ({ config, onChange }) => {
const handleAddOption = () => {
onChange({
diff --git a/src/entitiy/element/model/implementations/TextElement.tsx b/src/entitiy/element/model/implementations/TextElement.tsx
index 80bdd67..bc22bc7 100644
--- a/src/entitiy/element/model/implementations/TextElement.tsx
+++ b/src/entitiy/element/model/implementations/TextElement.tsx
@@ -22,6 +22,10 @@ export const textDefinition: ElementDefinition
= {
name: '',
},
mapToCells: config => config.targetCells || [],
+ mapToCellValues: (config, value) => {
+ const cells = config.targetCells || []
+ return cells.map(target => ({ target, value: value || '' }))
+ },
Editor: () => ,
Preview: ({ config }) => (
diff --git a/src/entitiy/element/model/interface.ts b/src/entitiy/element/model/interface.ts
index 91c2df8..298d1cb 100644
--- a/src/entitiy/element/model/interface.ts
+++ b/src/entitiy/element/model/interface.ts
@@ -23,7 +23,11 @@ export interface ElementDefinition
{
/* бизнес-логика */
defaultConfig: Config
- mapToCells(config: Config): CellTarget[] // куда пишем данные
+ mapToCells(config: Config): CellTarget[] // куда пишем данные (для отображения)
+ mapToCellValues(
+ config: Config,
+ value: Value
+ ): Array<{ target: CellTarget; value: any }> // готовые пары ячейка-значение для записи
/* мета */
version: number
diff --git a/src/hook/useFileQueries.ts b/src/hook/useFileQueries.ts
new file mode 100644
index 0000000..e53c389
--- /dev/null
+++ b/src/hook/useFileQueries.ts
@@ -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,
+ })
+ }
+}
diff --git a/src/hook/useMultiSheetEngine.ts b/src/hook/useMultiSheetEngine.ts
index 38883c0..2c26431 100644
--- a/src/hook/useMultiSheetEngine.ts
+++ b/src/hook/useMultiSheetEngine.ts
@@ -192,10 +192,10 @@ export function useMultiSheetEngine({
}
}
- // Запускаем безопасный пересчет
- safeRecalc()
+ // Запускаем отложенный пересчет с дебаунсом (избегаем двойного пересчёта)
+ debouncedRecalc()
},
- [safeRecalc]
+ [debouncedRecalc]
)
// Получение значения ячейки (вычисленное)
diff --git a/src/hook/useSheetAutoSave.ts b/src/hook/useSheetAutoSave.ts
index 8d5b0ce..efe232e 100644
--- a/src/hook/useSheetAutoSave.ts
+++ b/src/hook/useSheetAutoSave.ts
@@ -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(null)
- const [sheets, setSheets] = useState>({})
- 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>(
+ (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>(
- (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>) => {
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
> => {
const result: Record> = {}
- 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>((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),
}
}
diff --git a/src/hook/useSheetQueries.ts b/src/hook/useSheetQueries.ts
new file mode 100644
index 0000000..24b44cb
--- /dev/null
+++ b/src/hook/useSheetQueries.ts
@@ -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(
+ 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(
+ 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,
+ })
+ }
+}
diff --git a/src/lib/spreadsheet-engine/functions.ts b/src/lib/spreadsheet-engine/functions.ts
index bf10306..674037a 100644
--- a/src/lib/spreadsheet-engine/functions.ts
+++ b/src/lib/spreadsheet-engine/functions.ts
@@ -8,6 +8,7 @@ export interface ExcelFunction {
export const VOLATILE_FUNCTIONS = new Set([
'СЛУЧ',
'СЛУЧМЕЖДУ',
+ 'СЛУЧДР',
'СЕЙЧАС',
'СЕГОДНЯ',
])
@@ -16,6 +17,44 @@ export const DEFAULT_FUNCTIONS: Record = {
СЛУЧ: () => Math.random(),
СЛУЧМЕЖДУ: (низ: number, верх: number) =>
Math.floor(Math.random() * (верх - низ + 1)) + низ,
+ СЛУЧДР: (низ: number, верх: number, знаков: number = 0) => {
+ // Проверка валидности входных параметров
+ if (
+ typeof низ !== 'number' ||
+ typeof верх !== 'number' ||
+ typeof знаков !== 'number'
+ ) {
+ return '#ЗНАЧ!' // Ошибка значения
+ }
+
+ if (!isFinite(низ) || !isFinite(верх) || !isFinite(знаков)) {
+ return '#ЧИСЛО!' // Ошибка числа
+ }
+
+ // Если низ больше верха, меняем местами
+ if (низ > верх) {
+ ;[низ, верх] = [верх, низ]
+ }
+
+ // Если низ равен верху, возвращаем это значение
+ if (низ === верх) {
+ const множитель = Math.pow(10, Math.max(0, Math.floor(знаков)))
+ return Math.round(низ * множитель) / множитель
+ }
+
+ // Ограничиваем количество знаков после запятой (максимум 15 для точности JavaScript)
+ const ограниченныеЗнаки = Math.max(0, Math.min(15, Math.floor(знаков)))
+
+ // Генерируем случайное число
+ const случайное = Math.random() * (верх - низ) + низ
+ const множитель = Math.pow(10, ограниченныеЗнаки)
+
+ // Округляем и возвращаем результат
+ const результат = Math.round(случайное * множитель) / множитель
+
+ // Проверяем, что результат в допустимых пределах
+ return Math.max(низ, Math.min(верх, результат))
+ },
СУММ: (...args: any[]) => {
const flatArgs = args.flat()
return flatArgs.reduce(
diff --git a/src/page/ProtocolCreation/Page.tsx b/src/page/ProtocolCreation/Page.tsx
index cba9357..418ad8c 100644
--- a/src/page/ProtocolCreation/Page.tsx
+++ b/src/page/ProtocolCreation/Page.tsx
@@ -1,105 +1,15 @@
import DualSpreadsheet from '@/component/DualSpreadsheet/DualSpreadsheet'
-import { StandardsSelector } from '@/component/StandardsElement/StandardsSelector'
-import { Badge } from '@/component/ui/badge'
import { Button } from '@/component/ui/button'
-import { Checkbox } from '@/component/ui/checkbox'
-import { Input } from '@/component/ui/input'
-import { Label } from '@/component/ui/label'
-import { RadioGroup, RadioGroupItem } from '@/component/ui/radio-group'
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/component/ui/select'
-import { Textarea } from '@/component/ui/textarea'
import { getElementDefinition } from '@/entitiy/element/model/interface'
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
import { cellAddressToCoordinates } from '@/lib/cell-utils'
import { useToast } from '@/lib/hooks/useToast'
import { getLatestFileForTemplate } from '@/service/fileApiService'
import { Template, TemplateElement } from '@/type/template'
-import { ArrowLeft, FileText, Grid, Save, Settings, Wrench } from 'lucide-react'
+import { ArrowLeft, FileText, Grid, Save } from 'lucide-react'
import { FC, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
-// Моковые данные для эталонов (такие же как в StandardsSelector)
-const mockStandards = [
- {
- id: '1',
- name: 'Эталон массы 1 кг',
- shortName: 'ЭМ-1кг',
- type: 'Масса',
- registryNumber: 'ГРСИ 12345-01',
- range: '0.5-2 кг',
- accuracy: '±0.001 г',
- certificateNumber: 'СИ-2024-001',
- validUntil: '2025-12-31',
- },
- {
- id: '2',
- name: 'Эталон длины 1 м',
- shortName: 'ЭД-1м',
- type: 'Длина',
- registryNumber: 'ГРСИ 12345-02',
- range: '0.5-2 м',
- accuracy: '±0.001 мм',
- certificateNumber: 'СИ-2024-002',
- validUntil: '2025-06-30',
- },
- {
- id: '3',
- name: 'Эталон температуры 20°C',
- shortName: 'ЭТ-20°C',
- type: 'Температура',
- registryNumber: 'ГРСИ 12345-03',
- range: '15-25°C',
- accuracy: '±0.01°C',
- certificateNumber: 'СИ-2024-003',
- validUntil: '2025-03-15',
- },
- {
- id: '4',
- name: 'Эталон давления 1 Па',
- shortName: 'ЭД-1Па',
- type: 'Давление',
- registryNumber: 'ГРСИ 12345-04',
- range: '0.5-2 Па',
- accuracy: '±0.001 Па',
- certificateNumber: 'СИ-2024-004',
- validUntil: '2025-09-20',
- },
- {
- id: '5',
- name: 'Эталон времени 1 с',
- shortName: 'ЭВ-1с',
- type: 'Время',
- registryNumber: 'ГРСИ 12345-05',
- range: '0.5-2 с',
- accuracy: '±0.000001 с',
- certificateNumber: 'СИ-2024-005',
- validUntil: '2025-12-01',
- },
-]
-
-// Функция для получения данных эталона по ID
-const getStandardById = (id: string) => {
- return mockStandards.find(standard => standard.id === id)
-}
-
-// Функция для получения типа эталона в сокращенном виде
-const getStandardTypeBadge = (type: string) => {
- switch (type) {
- case 'Манометр грузопоршневой':
- return 'МГП'
- case 'Калибратор давления':
- return 'КД'
- default:
- return 'МО'
- }
-}
-
interface ProtocolFormProps {
template: Template
onSave: (data: Record) => void
@@ -113,297 +23,52 @@ interface FormElementProps {
}
const FormElement: FC = ({ element, value, onChange }) => {
- const [isStandardsDialogOpen, setIsStandardsDialogOpen] = useState(false)
+ // Получаем определение элемента из реестра
+ const elementDefinition = getElementDefinition(element.type)
- const renderInput = () => {
- switch (element.type) {
- case 'text':
- // Используем определение элемента из реестра для рендеринга
- const textDefinition = getElementDefinition('text')
- if (textDefinition && textDefinition.Render) {
- return (
-
- )
- }
- return (
- onChange(e.target.value)}
- required={element.required}
- />
- )
-
- case 'textarea':
- return (
-