diff --git a/src/component/BasicElements/CalibrationConditionsElement.tsx b/src/component/BasicElements/CalibrationConditionsElement.tsx index 8a726a6..5e5b1cf 100644 --- a/src/component/BasicElements/CalibrationConditionsElement.tsx +++ b/src/component/BasicElements/CalibrationConditionsElement.tsx @@ -3,6 +3,13 @@ import { Button } from '@/component/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card' import { Input } from '@/component/ui/input' import { Label } from '@/component/ui/label' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/component/ui/select' import { ElementDefinition } from '@/entitiy/element/model/interface' import { AlertCircle, @@ -412,6 +419,9 @@ function CalibrationCondInfo({ interface CalibrationConditionsConfig { targetCells: any[] defaultConditions?: CalibrationConditions + cellMapping?: { + [key: string]: string // параметр -> ячейка из targetCells (по индексу) + } } export const calibrationConditionsDefinition: ElementDefinition< @@ -424,6 +434,7 @@ export const calibrationConditionsDefinition: ElementDefinition< version: 1, defaultConfig: { targetCells: [], + cellMapping: {}, defaultConditions: { temperature: '20', humidity: '65', @@ -433,164 +444,289 @@ export const calibrationConditionsDefinition: ElementDefinition< lastUpdated: new Date().toISOString(), }, }, - mapToCells: () => { - // Генерируем ячейки для каждого параметра калибровки - const cells = [ - { sheet: 'L', cell: 'B2', displayName: 'Температура' }, - { sheet: 'L', cell: 'B3', displayName: 'Влажность' }, - { sheet: 'L', cell: 'B4', displayName: 'Давление' }, - { sheet: 'L', cell: 'B5', displayName: 'Напряжение' }, - { sheet: 'L', cell: 'B6', displayName: 'Частота' }, - { sheet: 'L', cell: 'B7', displayName: 'Дата обновления' }, - ] - return cells + mapToCells: cfg => { + // Используем только ячейки, указанные пользователем в targetCells + return cfg.targetCells || [] }, - Editor: ({ config, onChange }) => ( -
-
-

- Элемент "Условия калибровки" позволяет вводить и отслеживать параметры - окружающей среды при проведении измерений. Поддерживает валидацию - значений и отслеживание времени обновления. -

-
-
- -
+ 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 && (
- - - 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} +
+
+ +
+
+ ))} +
+

+ Настройте какой параметр условий калибровки будет записан в какую + ячейку. Используйте кнопку "Автозаполнение" для автоматического + сопоставления по порядку. +

-
- - - 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" - /> + )} + +
+ +
+
+ + + 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, 'cell', e.target.value) - } - className="h-8" - /> -
-
- - - handleUpdateTarget(index, 'displayName', e.target.value) - } - className="h-8" - /> -
-
- -
- ))} - {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({ startCell: e.target.value })} - /> -
-
-
- + +

- Ячейки для записи выбранных эталонов (автоматически генерируются) + Максимальное количество эталонов для выбора

- 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 ( +
+ {/* Блок с измерительными эталонами */} +
+
+ + +
+ +
+ {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 ( -