починка багов

This commit is contained in:
2025-07-23 16:08:50 +03:00
parent 3fc352f8e9
commit 907b3e315e
24 changed files with 1249 additions and 1430 deletions

View File

@@ -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 }) => (
<div className="space-y-4">
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
<p className="text-sm text-gray-600">
Элемент "Условия калибровки" позволяет вводить и отслеживать параметры
окружающей среды при проведении измерений. Поддерживает валидацию
значений и отслеживание времени обновления.
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Параметры по умолчанию</label>
<div className="grid grid-cols-2 gap-4 rounded-lg border border-gray-200 p-4">
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 (
<div className="space-y-4">
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
<p className="text-sm text-gray-600">
Элемент "Условия калибровки" позволяет вводить и отслеживать
параметры окружающей среды при проведении измерений. Поддерживает
валидацию значений и отслеживание времени обновления.
</p>
</div>
{/* Сопоставление параметров с ячейками */}
{targetCells.length > 0 && (
<div className="space-y-2">
<label className="text-xs font-medium text-gray-600">
Температура (°C)
</label>
<Input
type="number"
value={config.defaultConditions?.temperature || '20'}
onChange={e =>
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"
/>
<div className="flex items-center justify-between">
<Label className="text-sm font-medium">
Сопоставление с ячейками
</Label>
<Button
variant="outline"
size="sm"
onClick={createAutoMapping}
className="h-7 px-2 text-xs"
>
Автозаполнение
</Button>
</div>
<div className="space-y-3 rounded-lg border border-gray-200 p-4">
{availableParams.map(param => (
<div key={param.key} className="flex items-center gap-3">
<div className="min-w-[120px]">
<span className="text-sm text-gray-700">{param.label}</span>
</div>
<div className="flex-1">
<Select
value={cellMapping[param.key] || 'none'}
onValueChange={value =>
updateCellMapping(param.key, value)
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Не указано" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">Не указано</SelectItem>
{targetCells.map((cell, index) => (
<SelectItem key={index} value={index.toString()}>
{cell.sheet}!{cell.cell}{' '}
{cell.displayName ? `(${cell.displayName})` : ''}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
))}
</div>
<p className="text-xs text-gray-500">
Настройте какой параметр условий калибровки будет записан в какую
ячейку. Используйте кнопку "Автозаполнение" для автоматического
сопоставления по порядку.
</p>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-gray-600">
Влажность (%)
</label>
<Input
type="number"
value={config.defaultConditions?.humidity || '65'}
onChange={e =>
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"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-gray-600">
Давление (кПа)
</label>
<Input
type="number"
value={config.defaultConditions?.pressure || '101.3'}
onChange={e =>
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"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-gray-600">
Напряжение (В)
</label>
<Input
type="number"
value={config.defaultConditions?.voltage || '220'}
onChange={e =>
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"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-gray-600">
Частота (Гц)
</label>
<Input
type="number"
value={config.defaultConditions?.frequency || '50'}
onChange={e =>
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"
/>
)}
<div className="space-y-2">
<label className="text-sm font-medium">Параметры по умолчанию</label>
<div className="grid grid-cols-2 gap-4 rounded-lg border border-gray-200 p-4">
<div className="space-y-2">
<label className="text-xs font-medium text-gray-600">
Температура (°C)
</label>
<Input
type="number"
value={config.defaultConditions?.temperature || '20'}
onChange={e =>
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"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-gray-600">
Влажность (%)
</label>
<Input
type="number"
value={config.defaultConditions?.humidity || '65'}
onChange={e =>
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"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-gray-600">
Давление (кПа)
</label>
<Input
type="number"
value={config.defaultConditions?.pressure || '101.3'}
onChange={e =>
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"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-gray-600">
Напряжение (В)
</label>
<Input
type="number"
value={config.defaultConditions?.voltage || '220'}
onChange={e =>
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"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-gray-600">
Частота (Гц)
</label>
<Input
type="number"
value={config.defaultConditions?.frequency || '50'}
onChange={e =>
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"
/>
</div>
</div>
</div>
</div>
</div>
),
)
},
Preview: ({ config }) => (
<div className="space-y-3">
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
@@ -613,6 +749,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
</div>
),
Render: ({ config, value, onChange }) => {
// Если значение не установлено, используем defaultConditions из конфигурации
const conditions = value ||
config.defaultConditions || {
temperature: '20',

View File

@@ -18,6 +18,10 @@ export const checkboxDefinition: ElementDefinition<CheckboxConfig, boolean> = {
targetCells: [],
},
mapToCells: config => config.targetCells || [],
mapToCellValues: (config, value) => {
const cells = config.targetCells || []
return cells.map(target => ({ target, value: value || false }))
},
Editor: () => (
<div className="text-sm text-gray-600">
Дополнительные настройки отсутствуют

View File

@@ -17,6 +17,10 @@ export const dateDefinition: ElementDefinition<DateConfig, string> = {
targetCells: [],
},
mapToCells: config => config.targetCells || [],
mapToCellValues: (config, value) => {
const cells = config.targetCells || []
return cells.map(target => ({ target, value: value || '' }))
},
Editor: ({ config, onChange }) => (
<div className="space-y-4">
<div className="flex items-center space-x-2">

View File

@@ -19,6 +19,10 @@ export const numberDefinition: ElementDefinition<NumberConfig, number> = {
targetCells: [],
},
mapToCells: config => config.targetCells || [],
mapToCellValues: (config, value) => {
const cells = config.targetCells || []
return cells.map(target => ({ target, value: value || 0 }))
},
Editor: () => (
<div className="text-sm text-gray-600">
Дополнительные настройки отсутствуют

View File

@@ -23,6 +23,10 @@ export const radioDefinition: ElementDefinition<RadioConfig, string> = {
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({

View File

@@ -30,6 +30,10 @@ export const selectDefinition: ElementDefinition<SelectConfig, string> = {
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({

View File

@@ -19,6 +19,10 @@ export const textareaDefinition: ElementDefinition<TextareaConfig, string> = {
targetCells: [],
},
mapToCells: config => config.targetCells || [],
mapToCellValues: (config, value) => {
const cells = config.targetCells || []
return cells.map(target => ({ target, value: value || '' }))
},
Editor: () => (
<div className="text-sm text-gray-600">
Дополнительные настройки отсутствуют

View File

@@ -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<DualSpreadsheetProps> = ({
row: number
col: number
} | null>(null)
// Флаг, что пользователь изменил хотя бы одну ячейку
const [userEdited, setUserEdited] = useState(false)
// Состояние для множественного выделения
const [multiSelection, setMultiSelection] = useState<CellSelection[]>([])
const [isDragging, setIsDragging] = useState(false)
@@ -95,7 +99,14 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
>({}) // Базовые данные файла
const [highlightedCells, setHighlightedCells] = useState<HighlightedCell[]>(
[]
) // Подсвеченные ячейки для формул
)
// 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<DualSpreadsheetProps> = ({
const [columnWidths, setColumnWidths] =
useState<Record<SheetType, number[]>>(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<Record<SheetType, any>>(initialGridRefs)
const containerRefs =
useRef<Record<SheetType, HTMLDivElement | null>>(initialContainerRefs)
const headerRefs =
useRef<Record<SheetType, HTMLDivElement | null>>(initialHeaderRefs)
const rowHeaderRefs =
useRef<Record<SheetType, HTMLDivElement | null>>(initialRowHeaderRefs)
const gridRefs = useRef<Record<SheetType, any>>({
report: null,
calculations: null,
})
const containerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
report: null,
calculations: null,
})
const headerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
report: null,
calculations: null,
})
const rowHeaderRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
report: null,
calculations: null,
})
const activeCellInputRef = useRef<HTMLInputElement | null>(null)
const formulaBarRef = useRef<HTMLInputElement | null>(null)
@@ -171,22 +158,12 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
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<DualSpreadsheetProps> = ({
const getCachedCellData = useCallback(
(sheetType: SheetType): Record<string, CachedCellData> => {
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<DualSpreadsheetProps> = ({
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<DualSpreadsheetProps> = ({
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<DualSpreadsheetProps> = ({
)
setHighlightedCells(highlights)
// console.log('🎨 Подсветка ячеек для формулы:', formula, {
// references,
// highlights,
// })
} catch (error) {
console.error('Ошибка при обновлении подсветки ячеек:', error)
setHighlightedCells([])
@@ -422,23 +372,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
(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<DualSpreadsheetProps> = ({
(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<DualSpreadsheetProps> = ({
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<DualSpreadsheetProps> = ({
setEditingSource('cell')
setEditingValue(e.key)
setIsEditing(true)
if (!userEdited) setUserEdited(true)
}
},
[
@@ -610,13 +543,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
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<DualSpreadsheetProps> = ({
// Новый обработчик для кликов в режиме редактирования - вставляет ссылку
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<DualSpreadsheetProps> = ({
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<DualSpreadsheetProps> = ({
)
// Мемоизируем состояние ширин таблиц
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<DualSpreadsheetProps> = ({
lastSaveError,
manualSave,
clearError,
loadSavedSheets,
loadSheets, // Добавляем асинхронную функцию загрузки
loadSheets,
} = useSheetAutoSave(
memoizedGetAllModifiedCells,
multiSheetEngine.isCalculating,
@@ -924,33 +820,12 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
let finalBaseData: Record<string, Record<string, any>> = {}
// 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<DualSpreadsheetProps> = ({
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<DualSpreadsheetProps> = ({
}
})
// console.log(
// '📂 Загружены пользовательские изменения:',
// Object.keys(userChanges)
//)
// 4. ОБЪЕДИНЯЕМ БАЗОВЫЕ ДАННЫЕ С ПОЛЬЗОВАТЕЛЬСКИМИ ИЗМЕНЕНИЯМИ
if (Object.keys(userChanges).length > 0) {
// console.log(
// '🔄 Объединяем базовые данные с пользовательскими изменениями'
//)
// Создаем объединенные данные: базовые данные файла + пользовательские изменения
const mergedData: Record<string, Record<string, any>> = {}
@@ -1024,24 +885,15 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
})
})
// 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<DualSpreadsheetProps> = ({
}, [
templateId,
templateData,
fileData,
loadSheets,
multiSheetEngine,
setTemplateData,
@@ -1057,9 +910,22 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
])
// Загружаем сохраненные формулы при инициализации
// Ждем загрузки данных из React Query перед инициализацией
useEffect(() => {
initializeData()
}, [initializeData])
// Инициализируем только когда все данные загружены или их нет вообще
const canInitialize =
!isLoadingLatestFile && !isLoadingFileData && templateId && !isInitialized
if (canInitialize) {
initializeData()
}
}, [
initializeData,
isLoadingLatestFile,
isLoadingFileData,
templateId,
isInitialized,
])
return (
<div className="flex h-full flex-col bg-background">

View File

@@ -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 (
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Целевые ячейки</label>
<Button variant="outline" size="sm" onClick={handleAddTarget}>
<Calendar className="mr-1 h-4 w-4" />
Добавить ячейку
</Button>
</div>
<div className="max-h-48 space-y-3 overflow-y-auto">
{targets.map((target, index) => (
<div key={index} className="rounded-lg border bg-gray-50 p-3">
<div className="mb-2 grid grid-cols-3 gap-2">
<div className="space-y-1">
<label className="text-xs font-medium text-gray-600">
Лист
</label>
<Select
value={target.sheet}
onValueChange={value =>
handleUpdateTarget(index, 'sheet', value)
}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="L">L (Левый)</SelectItem>
<SelectItem value="R">R (Правый)</SelectItem>
<SelectItem value="Report">Report</SelectItem>
<SelectItem value="Calculations">Calculations</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-gray-600">
Ячейка
</label>
<Input
placeholder="A1, B5..."
value={target.cell}
onChange={e =>
handleUpdateTarget(index, 'cell', e.target.value)
}
className="h-8"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-gray-600">
Название
</label>
<Input
placeholder="Описание"
value={target.displayName || ''}
onChange={e =>
handleUpdateTarget(index, 'displayName', e.target.value)
}
className="h-8"
/>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => handleRemoveTarget(index)}
className="h-6 text-xs text-red-600 hover:text-red-700"
>
<Trash2 className="mr-1 h-3 w-3" />
Удалить
</Button>
</div>
))}
{targets.length === 0 && (
<p className="py-4 text-center text-sm text-gray-500">
Добавьте ячейки для связи с элементом
</p>
)}
</div>
</div>
)
}
export const StandardsEditor: React.FC<StandardsEditorProps> = ({
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<StandardsEditorProps> = ({
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Номер ГРСИ</label>
<Input
placeholder="Введите номер ГРСИ"
value={safeConfig.registryNumber}
onChange={e => updateConfig({ registryNumber: e.target.value })}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Максимум эталонов</label>
<Select
value={safeConfig.maxItems.toString()}
onValueChange={value => updateConfig({ maxItems: parseInt(value) })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="3">3 эталона</SelectItem>
<SelectItem value="5">5 эталонов</SelectItem>
<SelectItem value="7">7 эталонов</SelectItem>
<SelectItem value="10">10 эталонов</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Лист Excel</label>
<Select
value={safeConfig.sheet}
onValueChange={value => updateConfig({ sheet: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="L">L (Левый)</SelectItem>
<SelectItem value="R">R (Правый)</SelectItem>
<SelectItem value="Report">Report</SelectItem>
<SelectItem value="Calculations">Calculations</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Начальная ячейка</label>
<Input
placeholder="A1, B5..."
value={safeConfig.startCell}
onChange={e => updateConfig({ startCell: e.target.value })}
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Целевые ячейки</label>
<label className="text-sm font-medium">Максимум эталонов</label>
<Select
value={safeConfig.maxItems.toString()}
onValueChange={value => updateConfig({ maxItems: parseInt(value) })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="3">3 эталона</SelectItem>
<SelectItem value="5">5 эталонов</SelectItem>
<SelectItem value="7">7 эталонов</SelectItem>
<SelectItem value="10">10 эталонов</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-gray-500">
Ячейки для записи выбранных эталонов (автоматически генерируются)
Максимальное количество эталонов для выбора
</p>
<CellTargetEditor
targets={safeConfig.targetCells}
onChange={targets => updateConfig({ targetCells: targets })}
/>
</div>
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3">
<div className="flex items-start gap-2">
<Calendar className="mt-0.5 h-4 w-4 text-blue-600" />
<Info className="mt-0.5 h-4 w-4 text-blue-600" />
<div className="text-sm">
<div className="font-medium text-blue-800">
Автоматическое заполнение
</div>
<div className="font-medium text-blue-800">Настройка ячеек</div>
<div className="mt-1 text-xs text-blue-600">
При выборе эталонов данные будут записаны в указанные ячейки. Если
выбрано меньше эталонов, чем максимальное количество, оставшиеся
ячейки останутся пустыми.
Ячейки для размещения эталонов настраиваются в разделе "Целевые
ячейки" выше. Эталоны будут записываться в указанные ячейки по
порядку.
</div>
</div>
</div>

View File

@@ -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<StandardsPreviewProps> = ({
config,
}) => {
// Берем первые config.maxItems эталонов для предпросмотра
const previewStandards = mockStandards.slice(0, config.maxItems || 7)
return (
<div className="space-y-3">
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
<div className="rounded-lg border border-gray-200 bg-white p-4">
<div className="space-y-3">
<div className="flex items-center gap-2">
<Settings className="h-4 w-4 text-gray-500" />
<span className="text-sm font-medium">Измерительные эталоны</span>
</div>
<div className="space-y-2">
{/* Блок с измерительными эталонами */}
<div>
<div className="mb-2 flex items-center justify-between">
<Label className="text-sm font-medium">Эталоны</Label>
<Button variant="outline" size="sm" disabled className="h-7 px-2">
<Settings className="mr-1 h-3 w-3" />
Настроить
</Button>
</div>
<div className="space-y-2">
<div className="text-xs text-gray-500">
ГРСИ: {config.registryNumber || 'Не указан'}
</div>
<div className="text-xs text-gray-500">
Максимум: {config.maxItems} эталонов
</div>
<div className="space-y-2">
<div className="space-y-1.5">
{previewStandards.map((standard, index) => (
<div
key={standard.id}
className="flex items-center gap-2 rounded-md bg-muted/30 p-2 text-sm"
>
<div className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary/10">
<span className="text-xs font-medium text-primary">
{index + 1}
</span>
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-xs font-medium text-foreground">
{standard.shortName}
</div>
<div className="text-xs text-muted-foreground">
{standard.accuracy} {standard.range}
</div>
</div>
<Badge variant="secondary" className="shrink-0 text-xs">
<Wrench className="mr-1 h-3 w-3" />
{getStandardTypeBadge(standard.type)}
</Badge>
</div>
))}
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" disabled className="text-xs">
<Calendar className="mr-1 h-3 w-3" />
Выбрать эталоны
</Button>
<Badge variant="secondary" className="text-xs">
0/{config.maxItems} выбрано
</Badge>
</div>
{config.targetCells && config.targetCells.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{config.targetCells.map((target, index) => (
<span
key={index}
className="inline-block rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700"
title={target.displayName}
>
{target.sheet}!{target.cell}
</span>
))}
</div>
)}
</div>
</div>
</div>

View File

@@ -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<StandardsConfig, string[]> =
icon: <Weight className="h-4 w-4" />,
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<string[]>(
[]
)
// Если 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 (
<div className="space-y-2">
{/* Блок с измерительными эталонами */}
<div>
<div className="mb-2 flex items-center justify-between">
<Label className="text-sm font-medium">Эталоны</Label>
<Button
variant="outline"
size="sm"
onClick={() => setIsStandardsDialogOpen(true)}
className="h-7 px-2"
>
<Settings className="mr-1 h-3 w-3" />
Настроить
</Button>
</div>
<div className="space-y-2">
{selectedStandardsData.length > 0 ? (
<div className="space-y-1.5">
{selectedStandardsData.map(
(standard, index) =>
standard && (
<div
key={standard.id}
className="flex items-center gap-2 rounded-md bg-muted/30 p-2 text-sm"
>
<div className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary/10">
<span className="text-xs font-medium text-primary">
{index + 1}
</span>
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-xs font-medium text-foreground">
{standard.shortName}
</div>
<div className="text-xs text-muted-foreground">
{standard.accuracy} {standard.range}
</div>
</div>
<Badge
variant="secondary"
className="shrink-0 text-xs"
>
<Wrench className="mr-1 h-3 w-3" />
{getStandardTypeBadge(standard.type)}
</Badge>
</div>
)
)}
</div>
) : (
<div className="rounded-md border border-dashed bg-muted/30 p-3 text-center text-sm text-muted-foreground">
<Settings className="mx-auto mb-1 h-4 w-4 opacity-50" />
Эталоны не выбраны
</div>
)}
</div>
</div>
<StandardsSelector
isOpen={isStandardsDialogOpen}
onClose={() => setIsStandardsDialogOpen(false)}
value={selectedStandardIds}
onChange={standardIds => {
setSelectedStandardIds(standardIds)
// Преобразуем ID эталонов в их названия для сохранения в ячейки
const standardNames = standardIds.map(
id => getStandardById(id)?.name || ''
)
onChange?.(standardNames)
}}
/>
</div>
)
},
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] || '', // Берем значение по индексу или пустую строку
}))
},
}

View File

@@ -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 (
<div className="space-y-1.5">
<div className="flex items-center justify-between">
@@ -119,9 +126,7 @@ const CellTargetEditor: React.FC<{
<Input
placeholder="A1, B5..."
value={target.cell}
onChange={e =>
handleUpdateTarget(index, 'cell', e.target.value)
}
onChange={e => handleCellInputChange(index, e.target.value)}
className="h-7 text-xs"
/>
</div>
@@ -159,163 +164,7 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({
return <elementDefinition.Preview config={element as any} />
}
// Fallback для старых элементов
const renderPreviewInput = () => {
switch (element.type) {
case 'text':
return (
<Input
placeholder={element.placeholder || 'Введите текст'}
disabled
className="w-full"
/>
)
case 'textarea':
return (
<Textarea
placeholder={element.placeholder || 'Введите текст'}
disabled
rows={3}
className="w-full resize-none"
/>
)
case 'number':
return (
<Input
type="number"
placeholder={element.placeholder || '0'}
disabled
className="w-full"
/>
)
case 'date':
return <Input type="date" disabled className="w-full" />
case 'select':
return (
<Select disabled>
<SelectTrigger className="w-full">
<span className="text-gray-500">
{element.placeholder || 'Выберите значение'}
</span>
</SelectTrigger>
</Select>
)
case 'radio':
return (
<div className="space-y-2">
{element.options?.length ? (
element.options.map((option, index) => (
<div key={index} className="flex items-center space-x-2">
<div className="h-4 w-4 rounded-full border border-gray-300" />
<span className="text-sm">{option.label}</span>
</div>
))
) : (
<div className="flex items-center space-x-2">
<div className="h-4 w-4 rounded-full border border-gray-300" />
<span className="text-sm text-gray-500">Вариант 1</span>
</div>
)}
</div>
)
case 'checkbox':
return (
<div className="flex items-center space-x-2">
<div className="h-4 w-4 rounded border border-gray-300" />
<span className="text-sm">
{element.placeholder || element.label || 'Отметить'}
</span>
</div>
)
case 'standards':
return (
<div className="space-y-2">
<div className="flex items-center justify-between rounded-md border border-input bg-background p-3">
<span className="text-sm text-muted-foreground">
{element.placeholder || 'Выберите эталоны'}
</span>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 opacity-50" />
<span className="text-xs text-muted-foreground">Выбрать</span>
</div>
</div>
<div className="text-xs text-muted-foreground">
Выбрано эталонов: 0
</div>
</div>
)
case 'calibration_conditions':
return (
<div className="space-y-2">
<div className="flex items-center gap-3 rounded-lg border bg-muted/50 px-3 py-2">
<div className="flex items-center gap-3 text-xs">
<div className="flex items-center gap-1">
<Thermometer className="h-3 w-3 text-muted-foreground" />
<span className="text-foreground">20°C</span>
</div>
<div className="flex items-center gap-1">
<Droplets className="h-3 w-3 text-muted-foreground" />
<span className="text-foreground">65%</span>
</div>
<div className="flex items-center gap-1">
<Gauge className="h-3 w-3 text-muted-foreground" />
<span className="text-foreground">101.3кПа</span>
</div>
<div className="flex items-center gap-1">
<Zap className="h-3 w-3 text-muted-foreground" />
<span className="text-foreground">220В</span>
</div>
<div className="flex items-center gap-1">
<Radio className="h-3 w-3 text-muted-foreground" />
<span className="text-foreground">50Гц</span>
</div>
</div>
<Button variant="ghost" size="icon" className="h-6 w-6 p-0">
<Edit3 className="h-3 w-3" />
</Button>
</div>
</div>
)
case 'button_group':
return (
<div className="space-y-2">
<div className="flex flex-wrap gap-1.5">
{element.options?.length ? (
element.options.map((option, index) => (
<button
key={index}
className="rounded-md border border-border bg-background px-3 py-1.5 text-sm font-medium text-foreground transition-all duration-200 hover:border-primary/30 hover:bg-primary/5"
>
{option.label || `Кнопка ${index + 1}`}
</button>
))
) : (
<div className="rounded-md border border-dashed bg-muted/30 px-3 py-1.5 text-sm text-muted-foreground">
{element.placeholder || 'Добавьте кнопки'}
</div>
)}
</div>
</div>
)
default:
return (
<div className="rounded border border-gray-200 bg-gray-50 p-3 text-center text-sm text-gray-500">
Выберите тип элемента
</div>
)
}
}
// Fallback для неизвестных типов элементов
return (
<div className="space-y-3">
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
@@ -326,7 +175,13 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({
{element.required && <span className="ml-1 text-red-500">*</span>}
</label>
)}
{renderPreviewInput()}
<div className="rounded border border-gray-200 bg-gray-50 p-3 text-center text-sm text-gray-500">
{element.type
? `Элемент типа "${element.type}" не найден`
: 'Выберите тип элемента'}
</div>
{element.targetCells && element.targetCells.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{element.targetCells.map((target, index) => (
@@ -699,7 +554,6 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
showGrid: true,
}
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
const [editingElement, setEditingElement] = useState<TemplateElement | null>(
null
)

View File

@@ -165,7 +165,7 @@ export const ToastContainer: FC<ToastContainerProps> = ({
if (toasts.length === 0) return null
return (
<div className="pointer-events-none fixed bottom-4 left-4 z-50 flex w-full max-w-sm flex-col-reverse">
<div className="pointer-events-none fixed bottom-4 left-4 z-[100] flex w-full max-w-sm flex-col-reverse">
{toasts.map(toast => (
<div key={toast.id} className="pointer-events-auto">
<ToastComponent toast={toast} onRemove={onRemove} />