починка багов
This commit is contained in:
@@ -3,6 +3,13 @@ import { Button } from '@/component/ui/button'
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card'
|
||||||
import { Input } from '@/component/ui/input'
|
import { Input } from '@/component/ui/input'
|
||||||
import { Label } from '@/component/ui/label'
|
import { Label } from '@/component/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/component/ui/select'
|
||||||
import { ElementDefinition } from '@/entitiy/element/model/interface'
|
import { ElementDefinition } from '@/entitiy/element/model/interface'
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
@@ -412,6 +419,9 @@ function CalibrationCondInfo({
|
|||||||
interface CalibrationConditionsConfig {
|
interface CalibrationConditionsConfig {
|
||||||
targetCells: any[]
|
targetCells: any[]
|
||||||
defaultConditions?: CalibrationConditions
|
defaultConditions?: CalibrationConditions
|
||||||
|
cellMapping?: {
|
||||||
|
[key: string]: string // параметр -> ячейка из targetCells (по индексу)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const calibrationConditionsDefinition: ElementDefinition<
|
export const calibrationConditionsDefinition: ElementDefinition<
|
||||||
@@ -424,6 +434,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
|
|||||||
version: 1,
|
version: 1,
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
targetCells: [],
|
targetCells: [],
|
||||||
|
cellMapping: {},
|
||||||
defaultConditions: {
|
defaultConditions: {
|
||||||
temperature: '20',
|
temperature: '20',
|
||||||
humidity: '65',
|
humidity: '65',
|
||||||
@@ -433,164 +444,289 @@ export const calibrationConditionsDefinition: ElementDefinition<
|
|||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mapToCells: () => {
|
mapToCells: cfg => {
|
||||||
// Генерируем ячейки для каждого параметра калибровки
|
// Используем только ячейки, указанные пользователем в targetCells
|
||||||
const cells = [
|
return cfg.targetCells || []
|
||||||
{ 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
|
|
||||||
},
|
},
|
||||||
Editor: ({ config, onChange }) => (
|
mapToCellValues: (config, value) => {
|
||||||
<div className="space-y-4">
|
// Используем targetCells из конфигурации
|
||||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
const cells = config.targetCells || []
|
||||||
<p className="text-sm text-gray-600">
|
const cellMapping = config.cellMapping || {}
|
||||||
Элемент "Условия калибровки" позволяет вводить и отслеживать параметры
|
|
||||||
окружающей среды при проведении измерений. Поддерживает валидацию
|
// Если значение не объект, возвращаем пустые значения
|
||||||
значений и отслеживание времени обновления.
|
if (!value || typeof value !== 'object') {
|
||||||
</p>
|
return cells.map(target => ({
|
||||||
</div>
|
target,
|
||||||
<div className="space-y-2">
|
value: '',
|
||||||
<label className="text-sm font-medium">Параметры по умолчанию</label>
|
}))
|
||||||
<div className="grid grid-cols-2 gap-4 rounded-lg border border-gray-200 p-4">
|
}
|
||||||
|
|
||||||
|
// Создаем обратное сопоставление: индекс ячейки -> параметр
|
||||||
|
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">
|
<div className="space-y-2">
|
||||||
<label className="text-xs font-medium text-gray-600">
|
<div className="flex items-center justify-between">
|
||||||
Температура (°C)
|
<Label className="text-sm font-medium">
|
||||||
</label>
|
Сопоставление с ячейками
|
||||||
<Input
|
</Label>
|
||||||
type="number"
|
<Button
|
||||||
value={config.defaultConditions?.temperature || '20'}
|
variant="outline"
|
||||||
onChange={e =>
|
size="sm"
|
||||||
onChange({
|
onClick={createAutoMapping}
|
||||||
...config,
|
className="h-7 px-2 text-xs"
|
||||||
defaultConditions: {
|
>
|
||||||
...config.defaultConditions,
|
Автозаполнение
|
||||||
temperature: e.target.value,
|
</Button>
|
||||||
humidity: config.defaultConditions?.humidity || '65',
|
</div>
|
||||||
pressure: config.defaultConditions?.pressure || '101.3',
|
<div className="space-y-3 rounded-lg border border-gray-200 p-4">
|
||||||
voltage: config.defaultConditions?.voltage || '220',
|
{availableParams.map(param => (
|
||||||
frequency: config.defaultConditions?.frequency || '50',
|
<div key={param.key} className="flex items-center gap-3">
|
||||||
lastUpdated:
|
<div className="min-w-[120px]">
|
||||||
config.defaultConditions?.lastUpdated ||
|
<span className="text-sm text-gray-700">{param.label}</span>
|
||||||
new Date().toISOString(),
|
</div>
|
||||||
},
|
<div className="flex-1">
|
||||||
})
|
<Select
|
||||||
}
|
value={cellMapping[param.key] || 'none'}
|
||||||
placeholder="20"
|
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>
|
||||||
<div className="space-y-2">
|
)}
|
||||||
<label className="text-xs font-medium text-gray-600">
|
|
||||||
Влажность (%)
|
<div className="space-y-2">
|
||||||
</label>
|
<label className="text-sm font-medium">Параметры по умолчанию</label>
|
||||||
<Input
|
<div className="grid grid-cols-2 gap-4 rounded-lg border border-gray-200 p-4">
|
||||||
type="number"
|
<div className="space-y-2">
|
||||||
value={config.defaultConditions?.humidity || '65'}
|
<label className="text-xs font-medium text-gray-600">
|
||||||
onChange={e =>
|
Температура (°C)
|
||||||
onChange({
|
</label>
|
||||||
...config,
|
<Input
|
||||||
defaultConditions: {
|
type="number"
|
||||||
...config.defaultConditions,
|
value={config.defaultConditions?.temperature || '20'}
|
||||||
temperature: config.defaultConditions?.temperature || '20',
|
onChange={e =>
|
||||||
humidity: e.target.value,
|
onChange({
|
||||||
pressure: config.defaultConditions?.pressure || '101.3',
|
...config,
|
||||||
voltage: config.defaultConditions?.voltage || '220',
|
defaultConditions: {
|
||||||
frequency: config.defaultConditions?.frequency || '50',
|
...config.defaultConditions,
|
||||||
lastUpdated:
|
temperature: e.target.value,
|
||||||
config.defaultConditions?.lastUpdated ||
|
humidity: config.defaultConditions?.humidity || '65',
|
||||||
new Date().toISOString(),
|
pressure: config.defaultConditions?.pressure || '101.3',
|
||||||
},
|
voltage: config.defaultConditions?.voltage || '220',
|
||||||
})
|
frequency: config.defaultConditions?.frequency || '50',
|
||||||
}
|
lastUpdated:
|
||||||
placeholder="65"
|
config.defaultConditions?.lastUpdated ||
|
||||||
/>
|
new Date().toISOString(),
|
||||||
</div>
|
},
|
||||||
<div className="space-y-2">
|
})
|
||||||
<label className="text-xs font-medium text-gray-600">
|
}
|
||||||
Давление (кПа)
|
placeholder="20"
|
||||||
</label>
|
/>
|
||||||
<Input
|
</div>
|
||||||
type="number"
|
<div className="space-y-2">
|
||||||
value={config.defaultConditions?.pressure || '101.3'}
|
<label className="text-xs font-medium text-gray-600">
|
||||||
onChange={e =>
|
Влажность (%)
|
||||||
onChange({
|
</label>
|
||||||
...config,
|
<Input
|
||||||
defaultConditions: {
|
type="number"
|
||||||
...config.defaultConditions,
|
value={config.defaultConditions?.humidity || '65'}
|
||||||
temperature: config.defaultConditions?.temperature || '20',
|
onChange={e =>
|
||||||
humidity: config.defaultConditions?.humidity || '65',
|
onChange({
|
||||||
pressure: e.target.value,
|
...config,
|
||||||
voltage: config.defaultConditions?.voltage || '220',
|
defaultConditions: {
|
||||||
frequency: config.defaultConditions?.frequency || '50',
|
...config.defaultConditions,
|
||||||
lastUpdated:
|
temperature:
|
||||||
config.defaultConditions?.lastUpdated ||
|
config.defaultConditions?.temperature || '20',
|
||||||
new Date().toISOString(),
|
humidity: e.target.value,
|
||||||
},
|
pressure: config.defaultConditions?.pressure || '101.3',
|
||||||
})
|
voltage: config.defaultConditions?.voltage || '220',
|
||||||
}
|
frequency: config.defaultConditions?.frequency || '50',
|
||||||
placeholder="101.3"
|
lastUpdated:
|
||||||
/>
|
config.defaultConditions?.lastUpdated ||
|
||||||
</div>
|
new Date().toISOString(),
|
||||||
<div className="space-y-2">
|
},
|
||||||
<label className="text-xs font-medium text-gray-600">
|
})
|
||||||
Напряжение (В)
|
}
|
||||||
</label>
|
placeholder="65"
|
||||||
<Input
|
/>
|
||||||
type="number"
|
</div>
|
||||||
value={config.defaultConditions?.voltage || '220'}
|
<div className="space-y-2">
|
||||||
onChange={e =>
|
<label className="text-xs font-medium text-gray-600">
|
||||||
onChange({
|
Давление (кПа)
|
||||||
...config,
|
</label>
|
||||||
defaultConditions: {
|
<Input
|
||||||
...config.defaultConditions,
|
type="number"
|
||||||
temperature: config.defaultConditions?.temperature || '20',
|
value={config.defaultConditions?.pressure || '101.3'}
|
||||||
humidity: config.defaultConditions?.humidity || '65',
|
onChange={e =>
|
||||||
pressure: config.defaultConditions?.pressure || '101.3',
|
onChange({
|
||||||
voltage: e.target.value,
|
...config,
|
||||||
frequency: config.defaultConditions?.frequency || '50',
|
defaultConditions: {
|
||||||
lastUpdated:
|
...config.defaultConditions,
|
||||||
config.defaultConditions?.lastUpdated ||
|
temperature:
|
||||||
new Date().toISOString(),
|
config.defaultConditions?.temperature || '20',
|
||||||
},
|
humidity: config.defaultConditions?.humidity || '65',
|
||||||
})
|
pressure: e.target.value,
|
||||||
}
|
voltage: config.defaultConditions?.voltage || '220',
|
||||||
placeholder="220"
|
frequency: config.defaultConditions?.frequency || '50',
|
||||||
/>
|
lastUpdated:
|
||||||
</div>
|
config.defaultConditions?.lastUpdated ||
|
||||||
<div className="space-y-2">
|
new Date().toISOString(),
|
||||||
<label className="text-xs font-medium text-gray-600">
|
},
|
||||||
Частота (Гц)
|
})
|
||||||
</label>
|
}
|
||||||
<Input
|
placeholder="101.3"
|
||||||
type="number"
|
/>
|
||||||
value={config.defaultConditions?.frequency || '50'}
|
</div>
|
||||||
onChange={e =>
|
<div className="space-y-2">
|
||||||
onChange({
|
<label className="text-xs font-medium text-gray-600">
|
||||||
...config,
|
Напряжение (В)
|
||||||
defaultConditions: {
|
</label>
|
||||||
...config.defaultConditions,
|
<Input
|
||||||
temperature: config.defaultConditions?.temperature || '20',
|
type="number"
|
||||||
humidity: config.defaultConditions?.humidity || '65',
|
value={config.defaultConditions?.voltage || '220'}
|
||||||
pressure: config.defaultConditions?.pressure || '101.3',
|
onChange={e =>
|
||||||
voltage: config.defaultConditions?.voltage || '220',
|
onChange({
|
||||||
frequency: e.target.value,
|
...config,
|
||||||
lastUpdated:
|
defaultConditions: {
|
||||||
config.defaultConditions?.lastUpdated ||
|
...config.defaultConditions,
|
||||||
new Date().toISOString(),
|
temperature:
|
||||||
},
|
config.defaultConditions?.temperature || '20',
|
||||||
})
|
humidity: config.defaultConditions?.humidity || '65',
|
||||||
}
|
pressure: config.defaultConditions?.pressure || '101.3',
|
||||||
placeholder="50"
|
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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
),
|
},
|
||||||
Preview: ({ config }) => (
|
Preview: ({ config }) => (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
|
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
|
||||||
@@ -613,6 +749,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
Render: ({ config, value, onChange }) => {
|
Render: ({ config, value, onChange }) => {
|
||||||
|
// Если значение не установлено, используем defaultConditions из конфигурации
|
||||||
const conditions = value ||
|
const conditions = value ||
|
||||||
config.defaultConditions || {
|
config.defaultConditions || {
|
||||||
temperature: '20',
|
temperature: '20',
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export const checkboxDefinition: ElementDefinition<CheckboxConfig, boolean> = {
|
|||||||
targetCells: [],
|
targetCells: [],
|
||||||
},
|
},
|
||||||
mapToCells: config => config.targetCells || [],
|
mapToCells: config => config.targetCells || [],
|
||||||
|
mapToCellValues: (config, value) => {
|
||||||
|
const cells = config.targetCells || []
|
||||||
|
return cells.map(target => ({ target, value: value || false }))
|
||||||
|
},
|
||||||
Editor: () => (
|
Editor: () => (
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
Дополнительные настройки отсутствуют
|
Дополнительные настройки отсутствуют
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ export const dateDefinition: ElementDefinition<DateConfig, string> = {
|
|||||||
targetCells: [],
|
targetCells: [],
|
||||||
},
|
},
|
||||||
mapToCells: config => config.targetCells || [],
|
mapToCells: config => config.targetCells || [],
|
||||||
|
mapToCellValues: (config, value) => {
|
||||||
|
const cells = config.targetCells || []
|
||||||
|
return cells.map(target => ({ target, value: value || '' }))
|
||||||
|
},
|
||||||
Editor: ({ config, onChange }) => (
|
Editor: ({ config, onChange }) => (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ export const numberDefinition: ElementDefinition<NumberConfig, number> = {
|
|||||||
targetCells: [],
|
targetCells: [],
|
||||||
},
|
},
|
||||||
mapToCells: config => config.targetCells || [],
|
mapToCells: config => config.targetCells || [],
|
||||||
|
mapToCellValues: (config, value) => {
|
||||||
|
const cells = config.targetCells || []
|
||||||
|
return cells.map(target => ({ target, value: value || 0 }))
|
||||||
|
},
|
||||||
Editor: () => (
|
Editor: () => (
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
Дополнительные настройки отсутствуют
|
Дополнительные настройки отсутствуют
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export const radioDefinition: ElementDefinition<RadioConfig, string> = {
|
|||||||
targetCells: [],
|
targetCells: [],
|
||||||
},
|
},
|
||||||
mapToCells: config => config.targetCells || [],
|
mapToCells: config => config.targetCells || [],
|
||||||
|
mapToCellValues: (config, value) => {
|
||||||
|
const cells = config.targetCells || []
|
||||||
|
return cells.map(target => ({ target, value: value || '' }))
|
||||||
|
},
|
||||||
Editor: ({ config, onChange }) => {
|
Editor: ({ config, onChange }) => {
|
||||||
const handleAddOption = () => {
|
const handleAddOption = () => {
|
||||||
onChange({
|
onChange({
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export const selectDefinition: ElementDefinition<SelectConfig, string> = {
|
|||||||
targetCells: [],
|
targetCells: [],
|
||||||
},
|
},
|
||||||
mapToCells: config => config.targetCells || [],
|
mapToCells: config => config.targetCells || [],
|
||||||
|
mapToCellValues: (config, value) => {
|
||||||
|
const cells = config.targetCells || []
|
||||||
|
return cells.map(target => ({ target, value: value || '' }))
|
||||||
|
},
|
||||||
Editor: ({ config, onChange }) => {
|
Editor: ({ config, onChange }) => {
|
||||||
const handleAddOption = () => {
|
const handleAddOption = () => {
|
||||||
onChange({
|
onChange({
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ export const textareaDefinition: ElementDefinition<TextareaConfig, string> = {
|
|||||||
targetCells: [],
|
targetCells: [],
|
||||||
},
|
},
|
||||||
mapToCells: config => config.targetCells || [],
|
mapToCells: config => config.targetCells || [],
|
||||||
|
mapToCellValues: (config, value) => {
|
||||||
|
const cells = config.targetCells || []
|
||||||
|
return cells.map(target => ({ target, value: value || '' }))
|
||||||
|
},
|
||||||
Editor: () => (
|
Editor: () => (
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
Дополнительные настройки отсутствуют
|
Дополнительные настройки отсутствуют
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { useFileData, useLatestFileForTemplate } from '@/hook/useFileQueries'
|
||||||
import { useMultiSheetEngine } from '@/hook/useMultiSheetEngine'
|
import { useMultiSheetEngine } from '@/hook/useMultiSheetEngine'
|
||||||
import { useSheetAutoSave } from '@/hook/useSheetAutoSave'
|
import { useSheetAutoSave } from '@/hook/useSheetAutoSave'
|
||||||
import { getFileData, getLatestFileForTemplate } from '@/service/fileApiService'
|
|
||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import React, {
|
import React, {
|
||||||
FC,
|
FC,
|
||||||
@@ -78,6 +78,10 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
row: number
|
row: number
|
||||||
col: number
|
col: number
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
|
// Флаг, что пользователь изменил хотя бы одну ячейку
|
||||||
|
const [userEdited, setUserEdited] = useState(false)
|
||||||
|
|
||||||
// Состояние для множественного выделения
|
// Состояние для множественного выделения
|
||||||
const [multiSelection, setMultiSelection] = useState<CellSelection[]>([])
|
const [multiSelection, setMultiSelection] = useState<CellSelection[]>([])
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
@@ -95,7 +99,14 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
>({}) // Базовые данные файла
|
>({}) // Базовые данные файла
|
||||||
const [highlightedCells, setHighlightedCells] = useState<HighlightedCell[]>(
|
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(
|
const initialColumnWidths = useMemo(
|
||||||
@@ -109,46 +120,22 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
const [columnWidths, setColumnWidths] =
|
const [columnWidths, setColumnWidths] =
|
||||||
useState<Record<SheetType, number[]>>(initialColumnWidths)
|
useState<Record<SheetType, number[]>>(initialColumnWidths)
|
||||||
|
|
||||||
// Мемоизируем начальные refs объекты
|
const gridRefs = useRef<Record<SheetType, any>>({
|
||||||
const initialGridRefs = useMemo(
|
report: null,
|
||||||
() => ({
|
calculations: null,
|
||||||
report: null,
|
})
|
||||||
calculations: null,
|
const containerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||||||
}),
|
report: null,
|
||||||
[]
|
calculations: null,
|
||||||
)
|
})
|
||||||
|
const headerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||||||
const initialContainerRefs = useMemo(
|
report: null,
|
||||||
() => ({
|
calculations: null,
|
||||||
report: null,
|
})
|
||||||
calculations: null,
|
const rowHeaderRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||||||
}),
|
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 activeCellInputRef = useRef<HTMLInputElement | null>(null)
|
const activeCellInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const formulaBarRef = useRef<HTMLInputElement | null>(null)
|
const formulaBarRef = useRef<HTMLInputElement | null>(null)
|
||||||
@@ -171,22 +158,12 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
initialTemplateValues.current[cellAddress] = String(value || '')
|
initialTemplateValues.current[cellAddress] = String(value || '')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
// console.log(
|
|
||||||
// '✅ Загружено значений baseFileData для сравнения (приоритет):',
|
|
||||||
// Object.keys(initialTemplateValues.current).length
|
|
||||||
//)
|
|
||||||
} else if (hasTemplateData) {
|
} else if (hasTemplateData) {
|
||||||
Object.entries(templateData).forEach(([sheetName, sheetData]) => {
|
Object.entries(templateData).forEach(([sheetName, sheetData]) => {
|
||||||
Object.entries(sheetData).forEach(([cellAddress, value]) => {
|
Object.entries(sheetData).forEach(([cellAddress, value]) => {
|
||||||
initialTemplateValues.current[cellAddress] = String(value || '')
|
initialTemplateValues.current[cellAddress] = String(value || '')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
// console.log(
|
|
||||||
// '✅ Загружено значений templateData для сравнения (fallback):',
|
|
||||||
// Object.keys(initialTemplateValues.current).length
|
|
||||||
// )
|
|
||||||
} else {
|
|
||||||
// console.log('⚠️ DualSpreadsheet: Нет базовых данных для сравнения')
|
|
||||||
}
|
}
|
||||||
}, [baseFileData, templateData])
|
}, [baseFileData, templateData])
|
||||||
|
|
||||||
@@ -212,7 +189,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
|
|
||||||
const getCachedCellData = useCallback(
|
const getCachedCellData = useCallback(
|
||||||
(sheetType: SheetType): Record<string, CachedCellData> => {
|
(sheetType: SheetType): Record<string, CachedCellData> => {
|
||||||
const cacheKey = `${sheetType}-${revision}-${JSON.stringify(columnWidths[sheetType])}`
|
const cacheKey = `${sheetType}-${revision}-${columnWidths[sheetType].join('-')}`
|
||||||
|
|
||||||
if (cellDataCache.current[cacheKey]) {
|
if (cellDataCache.current[cacheKey]) {
|
||||||
return cellDataCache.current[cacheKey]
|
return cellDataCache.current[cacheKey]
|
||||||
@@ -262,19 +239,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
const cellName = getColumnLabel(col) + (row + 1)
|
const cellName = getColumnLabel(col) + (row + 1)
|
||||||
const baseFileValue = initialTemplateValues.current[cellName] ?? '' // Базовое значение из файла
|
const baseFileValue = initialTemplateValues.current[cellName] ?? '' // Базовое значение из файла
|
||||||
|
|
||||||
// Отладка только для измененных ячеек
|
|
||||||
if (
|
|
||||||
sheetType === 'report' &&
|
|
||||||
baseFileValue &&
|
|
||||||
rawValue !== baseFileValue
|
|
||||||
) {
|
|
||||||
// console.log(`🔧 Измененная ячейка ${cellName}:`, {
|
|
||||||
// baseFileValue,
|
|
||||||
// rawValue,
|
|
||||||
// isModified: true,
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Нормализуем значения для сравнения
|
// Нормализуем значения для сравнения
|
||||||
const normalizeValue = (value: any): string => {
|
const normalizeValue = (value: any): string => {
|
||||||
if (value === null || value === undefined) return ''
|
if (value === null || value === undefined) return ''
|
||||||
@@ -290,15 +254,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
const isModified =
|
const isModified =
|
||||||
sheetType === 'report' && normalizedRaw !== normalizedBaseFile
|
sheetType === 'report' && normalizedRaw !== normalizedBaseFile
|
||||||
|
|
||||||
// Отладка только для измененных ячеек
|
|
||||||
if (sheetType === 'report' && isModified) {
|
|
||||||
// console.log(`🔍 Измененная ячейка ${cellName}:`, {
|
|
||||||
// baseFileValue,
|
|
||||||
// rawValue,
|
|
||||||
// isModified,
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedData[cellKey] = {
|
cachedData[cellKey] = {
|
||||||
displayValue,
|
displayValue,
|
||||||
rawValue,
|
rawValue,
|
||||||
@@ -343,11 +298,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
setHighlightedCells(highlights)
|
setHighlightedCells(highlights)
|
||||||
|
|
||||||
// console.log('🎨 Подсветка ячеек для формулы:', formula, {
|
|
||||||
// references,
|
|
||||||
// highlights,
|
|
||||||
// })
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при обновлении подсветки ячеек:', error)
|
console.error('Ошибка при обновлении подсветки ячеек:', error)
|
||||||
setHighlightedCells([])
|
setHighlightedCells([])
|
||||||
@@ -422,23 +372,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
(save: boolean) => {
|
(save: boolean) => {
|
||||||
if (save && selectedCell) {
|
if (save && selectedCell) {
|
||||||
const sheetName = SHEET_NAMES[activeSheet]
|
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(
|
multiSheetEngine.setCellValueWithoutRecalc(
|
||||||
sheetName,
|
sheetName,
|
||||||
@@ -461,23 +394,25 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
setEditingValue(newValue)
|
setEditingValue(newValue)
|
||||||
|
if (!userEdited) setUserEdited(true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isEditing]
|
[isEditing, userEdited]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleClearCell = useCallback(() => {
|
const handleClearCell = useCallback(() => {
|
||||||
if (!selectedCell) return
|
if (!selectedCell) return
|
||||||
const sheetName = SHEET_NAMES[activeSheet]
|
const sheetName = SHEET_NAMES[activeSheet]
|
||||||
|
|
||||||
multiSheetEngine.setCellValueWithoutRecalc(
|
multiSheetEngine.setCellValueWithoutRecalc(
|
||||||
sheetName,
|
sheetName,
|
||||||
selectedCell.row,
|
selectedCell.row,
|
||||||
selectedCell.col,
|
selectedCell.col,
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
|
setUserEdited(true)
|
||||||
updateRevision()
|
updateRevision()
|
||||||
multiSheetEngine.debouncedRecalc()
|
multiSheetEngine.debouncedRecalc()
|
||||||
// Сбрасываем множественное выделение при очистке одиночной ячейки
|
|
||||||
setMultiSelection([])
|
setMultiSelection([])
|
||||||
}, [activeSheet, selectedCell, multiSheetEngine, updateRevision])
|
}, [activeSheet, selectedCell, multiSheetEngine, updateRevision])
|
||||||
|
|
||||||
@@ -515,22 +450,19 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
const sheetName = SHEET_NAMES[activeSheet]
|
const sheetName = SHEET_NAMES[activeSheet]
|
||||||
const cellsToDelete = getAllCellsInSelections(multiSelection)
|
const cellsToDelete = getAllCellsInSelections(multiSelection)
|
||||||
|
|
||||||
cellsToDelete.forEach(({ row, col }: { row: number; col: number }) => {
|
cellsToDelete.forEach(({ row, col }) => {
|
||||||
multiSheetEngine.setCellValueWithoutRecalc(sheetName, row, col, '')
|
multiSheetEngine.setCellValueWithoutRecalc(sheetName, row, col, '')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setUserEdited(true)
|
||||||
updateRevision()
|
updateRevision()
|
||||||
multiSheetEngine.debouncedRecalc()
|
multiSheetEngine.debouncedRecalc()
|
||||||
|
|
||||||
// Очищаем выделение после удаления
|
|
||||||
setMultiSelection([])
|
setMultiSelection([])
|
||||||
}, [multiSelection, activeSheet, multiSheetEngine, updateRevision])
|
}, [multiSelection, activeSheet, multiSheetEngine, updateRevision])
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent) => {
|
(e: React.KeyboardEvent) => {
|
||||||
if (isEditing) {
|
if (isEditing) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedCell && e.key.match(/^(Arrow|Enter|F2|Delete|Backspace)$/)) {
|
if (!selectedCell && e.key.match(/^(Arrow|Enter|F2|Delete|Backspace)$/)) {
|
||||||
moveSelection(0, 0)
|
moveSelection(0, 0)
|
||||||
@@ -595,6 +527,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
setEditingSource('cell')
|
setEditingSource('cell')
|
||||||
setEditingValue(e.key)
|
setEditingValue(e.key)
|
||||||
setIsEditing(true)
|
setIsEditing(true)
|
||||||
|
if (!userEdited) setUserEdited(true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -610,13 +543,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
|
|
||||||
const handleCellClick = useCallback(
|
const handleCellClick = useCallback(
|
||||||
(row: number, col: number, event?: React.MouseEvent) => {
|
(row: number, col: number, event?: React.MouseEvent) => {
|
||||||
// console.log('🖱️ Обычный клик по ячейке:', {
|
|
||||||
// row,
|
|
||||||
//col,
|
|
||||||
//isEditing,
|
|
||||||
//activeSheet,
|
|
||||||
//})
|
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
stopEditing(true)
|
stopEditing(true)
|
||||||
}
|
}
|
||||||
@@ -710,44 +636,23 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
// Новый обработчик для кликов в режиме редактирования - вставляет ссылку
|
// Новый обработчик для кликов в режиме редактирования - вставляет ссылку
|
||||||
const handleCellReferenceClick = useCallback(
|
const handleCellReferenceClick = useCallback(
|
||||||
(targetSheetType: SheetType, row: number, col: number) => {
|
(targetSheetType: SheetType, row: number, col: number) => {
|
||||||
// console.log('🔗 Клик для вставки ссылки:', {
|
if (!isEditing || !selectedCell || !editingValue.startsWith('=')) {
|
||||||
//targetSheetType,
|
stopEditing(true)
|
||||||
//row,
|
setSelectedCell({ row, col })
|
||||||
//col,
|
|
||||||
//isEditing,
|
|
||||||
//selectedCell,
|
|
||||||
//editingValue,
|
|
||||||
//})
|
|
||||||
|
|
||||||
// Проверяем, что мы в режиме редактирования
|
|
||||||
if (!isEditing || !selectedCell) {
|
|
||||||
// console.log(
|
|
||||||
// '⚠️ Игнорируем клик ссылки: не в режиме редактирования или нет выбранной ячейки'
|
|
||||||
//)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем ссылку на ячейку
|
|
||||||
let cellReference: string
|
let cellReference: string
|
||||||
|
|
||||||
// Если это другой лист, делаем квалифицированную ссылку
|
|
||||||
if (targetSheetType !== activeSheet) {
|
if (targetSheetType !== activeSheet) {
|
||||||
cellReference = createQualifiedCellReference(targetSheetType, row, col)
|
cellReference = createQualifiedCellReference(targetSheetType, row, col)
|
||||||
// console.log('📋 Создана межлистовая ссылка:', cellReference)
|
|
||||||
} else {
|
} else {
|
||||||
// Если это тот же лист, делаем локальную ссылку
|
|
||||||
cellReference = createCellAddress(row, col)
|
cellReference = createCellAddress(row, col)
|
||||||
// console.log('📋 Создана локальная ссылка:', cellReference)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем ссылку к текущему значению редактирования
|
|
||||||
const newValue = editingValue + cellReference
|
const newValue = editingValue + cellReference
|
||||||
// console.log('✏️ Новое значение формулы:', {
|
|
||||||
//oldValue: editingValue,
|
|
||||||
//newValue,
|
|
||||||
//})
|
|
||||||
|
|
||||||
setEditingValue(newValue)
|
setEditingValue(newValue)
|
||||||
|
if (!userEdited) setUserEdited(true)
|
||||||
|
|
||||||
if (editingSource === 'formula') {
|
if (editingSource === 'formula') {
|
||||||
if (formulaBarRef.current) {
|
if (formulaBarRef.current) {
|
||||||
@@ -795,13 +700,12 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
const handleFormulaBarChange = useCallback(
|
const handleFormulaBarChange = useCallback(
|
||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
if (selectedCell) {
|
if (selectedCell) {
|
||||||
if (!isEditing) {
|
if (!isEditing) setIsEditing(true)
|
||||||
setIsEditing(true)
|
|
||||||
}
|
|
||||||
setEditingValue(newValue)
|
setEditingValue(newValue)
|
||||||
|
if (!userEdited) setUserEdited(true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[selectedCell, isEditing]
|
[selectedCell, isEditing, userEdited]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFormulaBarFocus = useCallback(() => {
|
const handleFormulaBarFocus = useCallback(() => {
|
||||||
@@ -860,32 +764,25 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Мемоизируем состояние ширин таблиц
|
// Мемоизируем состояние ширин таблиц
|
||||||
const initialSpreadsheetWidths = useMemo(
|
const [spreadsheetWidths, setSpreadsheetWidths] = useState({
|
||||||
() => ({
|
L: 0.5,
|
||||||
L: 0.5,
|
R: 0.5,
|
||||||
R: 0.5,
|
})
|
||||||
}),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const [spreadsheetWidths, setSpreadsheetWidths] = useState(
|
|
||||||
initialSpreadsheetWidths
|
|
||||||
)
|
|
||||||
|
|
||||||
// Мемоизируем опции автосохранения
|
// Мемоизируем опции автосохранения
|
||||||
const autoSaveOptions = useMemo(
|
const autoSaveOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
templateId,
|
templateId,
|
||||||
autoSaveDelay: 2000,
|
autoSaveDelay: 2000,
|
||||||
enableAutoSave,
|
enableAutoSave: enableAutoSave && userEdited,
|
||||||
}),
|
}),
|
||||||
[templateId, enableAutoSave]
|
[templateId, enableAutoSave, userEdited]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Мемоизируем getAllModifiedCells для предотвращения частых ререндеров хука автосохранения
|
// Мемоизируем getAllModifiedCells для предотвращения частых ререндеров хука автосохранения
|
||||||
const memoizedGetAllModifiedCells = useCallback(
|
const memoizedGetAllModifiedCells = useCallback(
|
||||||
multiSheetEngine.getAllModifiedCells,
|
multiSheetEngine.getAllModifiedCells,
|
||||||
[multiSheetEngine.getAllModifiedCells]
|
[revision] // Используем revision вместо функции для более стабильной мемоизации
|
||||||
)
|
)
|
||||||
|
|
||||||
// Используем новый хук автосохранения
|
// Используем новый хук автосохранения
|
||||||
@@ -896,8 +793,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
lastSaveError,
|
lastSaveError,
|
||||||
manualSave,
|
manualSave,
|
||||||
clearError,
|
clearError,
|
||||||
loadSavedSheets,
|
loadSheets,
|
||||||
loadSheets, // Добавляем асинхронную функцию загрузки
|
|
||||||
} = useSheetAutoSave(
|
} = useSheetAutoSave(
|
||||||
memoizedGetAllModifiedCells,
|
memoizedGetAllModifiedCells,
|
||||||
multiSheetEngine.isCalculating,
|
multiSheetEngine.isCalculating,
|
||||||
@@ -924,33 +820,12 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
|
|
||||||
let finalBaseData: Record<string, Record<string, any>> = {}
|
let finalBaseData: Record<string, Record<string, any>> = {}
|
||||||
|
|
||||||
// 1. ЗАГРУЖАЕМ БАЗОВЫЕ ДАННЫЕ ФАЙЛА
|
// 1. ИСПОЛЬЗУЕМ ДАННЫЕ ИЗ REACT QUERY КЕША
|
||||||
if (templateId) {
|
if (fileData && Object.keys(fileData.data).length > 0) {
|
||||||
try {
|
finalBaseData = { [SHEET_NAMES.report]: fileData.data }
|
||||||
const latestFile = await getLatestFileForTemplate(templateId)
|
setBaseFileData(finalBaseData)
|
||||||
if (latestFile) {
|
// Сохраняем baseline, чтобы в листы попадали только пользовательские изменения
|
||||||
// console.log('📁 Найден последний файл для шаблона:', latestFile.name)
|
setTemplateData(finalBaseData)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. ИСПОЛЬЗУЕМ templateData КАК FALLBACK или основные данные
|
// 2. ИСПОЛЬЗУЕМ templateData КАК FALLBACK или основные данные
|
||||||
@@ -961,16 +836,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
if (hasTemplateData && Object.keys(finalBaseData).length === 0) {
|
if (hasTemplateData && Object.keys(finalBaseData).length === 0) {
|
||||||
finalBaseData = templateData
|
finalBaseData = templateData
|
||||||
setBaseFileData(finalBaseData)
|
setBaseFileData(finalBaseData)
|
||||||
// console.log(
|
|
||||||
// '📄 Используем templateData как базовые данные:',
|
|
||||||
// finalBaseData
|
|
||||||
//)
|
|
||||||
setTemplateData(templateData)
|
setTemplateData(templateData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. ЗАГРУЖАЕМ ПОЛЬЗОВАТЕЛЬСКИЕ ИЗМЕНЕНИЯ ИЗ API
|
// 3. ЗАГРУЖАЕМ ПОЛЬЗОВАТЕЛЬСКИЕ ИЗМЕНЕНИЯ ИЗ API
|
||||||
try {
|
try {
|
||||||
// console.log('🔍 Загружаем пользовательские изменения из API...')
|
|
||||||
const apiSheets = await loadSheets()
|
const apiSheets = await loadSheets()
|
||||||
|
|
||||||
// Преобразуем листы API в формат данных
|
// Преобразуем листы API в формат данных
|
||||||
@@ -995,17 +865,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// console.log(
|
|
||||||
// '📂 Загружены пользовательские изменения:',
|
|
||||||
// Object.keys(userChanges)
|
|
||||||
//)
|
|
||||||
|
|
||||||
// 4. ОБЪЕДИНЯЕМ БАЗОВЫЕ ДАННЫЕ С ПОЛЬЗОВАТЕЛЬСКИМИ ИЗМЕНЕНИЯМИ
|
// 4. ОБЪЕДИНЯЕМ БАЗОВЫЕ ДАННЫЕ С ПОЛЬЗОВАТЕЛЬСКИМИ ИЗМЕНЕНИЯМИ
|
||||||
if (Object.keys(userChanges).length > 0) {
|
if (Object.keys(userChanges).length > 0) {
|
||||||
// console.log(
|
|
||||||
// '🔄 Объединяем базовые данные с пользовательскими изменениями'
|
|
||||||
//)
|
|
||||||
|
|
||||||
// Создаем объединенные данные: базовые данные файла + пользовательские изменения
|
// Создаем объединенные данные: базовые данные файла + пользовательские изменения
|
||||||
const mergedData: Record<string, Record<string, any>> = {}
|
const mergedData: Record<string, Record<string, any>> = {}
|
||||||
|
|
||||||
@@ -1024,24 +885,15 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// console.log(
|
|
||||||
// '📊 Загружаем объединенные данные (базовые + пользовательские)'
|
|
||||||
//)
|
|
||||||
multiSheetEngine.loadData(mergedData)
|
multiSheetEngine.loadData(mergedData)
|
||||||
} else if (Object.keys(finalBaseData).length > 0) {
|
} else if (Object.keys(finalBaseData).length > 0) {
|
||||||
// console.log(
|
|
||||||
// '📋 Используем только базовые данные (нет пользовательских изменений)'
|
|
||||||
//)
|
|
||||||
multiSheetEngine.loadData(finalBaseData)
|
multiSheetEngine.loadData(finalBaseData)
|
||||||
} else {
|
|
||||||
// console.log('⚠️ Нет данных для загрузки')
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Ошибка при загрузке пользовательских изменений:', error)
|
console.error('❌ Ошибка при загрузке пользовательских изменений:', error)
|
||||||
|
|
||||||
// В случае ошибки используем только базовые данные
|
// В случае ошибки используем только базовые данные
|
||||||
if (Object.keys(finalBaseData).length > 0) {
|
if (Object.keys(finalBaseData).length > 0) {
|
||||||
// console.log('📋 Ошибка API, используем только базовые данные')
|
|
||||||
multiSheetEngine.loadData(finalBaseData)
|
multiSheetEngine.loadData(finalBaseData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1050,6 +902,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
}, [
|
}, [
|
||||||
templateId,
|
templateId,
|
||||||
templateData,
|
templateData,
|
||||||
|
fileData,
|
||||||
loadSheets,
|
loadSheets,
|
||||||
multiSheetEngine,
|
multiSheetEngine,
|
||||||
setTemplateData,
|
setTemplateData,
|
||||||
@@ -1057,9 +910,22 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
])
|
])
|
||||||
|
|
||||||
// Загружаем сохраненные формулы при инициализации
|
// Загружаем сохраненные формулы при инициализации
|
||||||
|
// Ждем загрузки данных из React Query перед инициализацией
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializeData()
|
// Инициализируем только когда все данные загружены или их нет вообще
|
||||||
}, [initializeData])
|
const canInitialize =
|
||||||
|
!isLoadingLatestFile && !isLoadingFileData && templateId && !isInitialized
|
||||||
|
|
||||||
|
if (canInitialize) {
|
||||||
|
initializeData()
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
initializeData,
|
||||||
|
isLoadingLatestFile,
|
||||||
|
isLoadingFileData,
|
||||||
|
templateId,
|
||||||
|
isInitialized,
|
||||||
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col bg-background">
|
<div className="flex h-full flex-col bg-background">
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { Button } from '@/component/ui/button'
|
|
||||||
import { Input } from '@/component/ui/input'
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -8,10 +6,9 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/component/ui/select'
|
} from '@/component/ui/select'
|
||||||
import { CellTarget } from '@/type/template'
|
import { CellTarget } from '@/type/template'
|
||||||
import { Calendar, Settings, Trash2 } from 'lucide-react'
|
import { Info, Settings } from 'lucide-react'
|
||||||
|
|
||||||
interface StandardsConfig {
|
interface StandardsConfig {
|
||||||
registryNumber: string // ГРСИ
|
|
||||||
sheet: string // Лист Excel
|
sheet: string // Лист Excel
|
||||||
startCell: string // Первая ячейка (например "A10")
|
startCell: string // Первая ячейка (например "A10")
|
||||||
maxItems: number // Обычно 7
|
maxItems: number // Обычно 7
|
||||||
@@ -23,119 +20,13 @@ interface StandardsEditorProps {
|
|||||||
onChange: (config: StandardsConfig) => void
|
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> = ({
|
export const StandardsEditor: React.FC<StandardsEditorProps> = ({
|
||||||
config,
|
config,
|
||||||
onChange,
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
// Обеспечиваем значения по умолчанию
|
// Обеспечиваем значения по умолчанию
|
||||||
const safeConfig = {
|
const safeConfig = {
|
||||||
registryNumber: config.registryNumber || '',
|
sheet: config.sheet || 'L',
|
||||||
sheet: config.sheet || 'Report',
|
|
||||||
startCell: config.startCell || 'A1',
|
startCell: config.startCell || 'A1',
|
||||||
maxItems: config.maxItems || 7,
|
maxItems: config.maxItems || 7,
|
||||||
targetCells: config.targetCells || [],
|
targetCells: config.targetCells || [],
|
||||||
@@ -157,86 +48,36 @@ export const StandardsEditor: React.FC<StandardsEditorProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<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 className="text-xs text-gray-500">
|
||||||
Ячейки для записи выбранных эталонов (автоматически генерируются)
|
Максимальное количество эталонов для выбора
|
||||||
</p>
|
</p>
|
||||||
<CellTargetEditor
|
|
||||||
targets={safeConfig.targetCells}
|
|
||||||
onChange={targets => updateConfig({ targetCells: targets })}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3">
|
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||||
<div className="flex items-start gap-2">
|
<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="text-sm">
|
||||||
<div className="font-medium text-blue-800">
|
<div className="font-medium text-blue-800">Настройка ячеек</div>
|
||||||
Автоматическое заполнение
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-xs text-blue-600">
|
<div className="mt-1 text-xs text-blue-600">
|
||||||
При выборе эталонов данные будут записаны в указанные ячейки. Если
|
Ячейки для размещения эталонов настраиваются в разделе "Целевые
|
||||||
выбрано меньше эталонов, чем максимальное количество, оставшиеся
|
ячейки" выше. Эталоны будут записываться в указанные ячейки по
|
||||||
ячейки останутся пустыми.
|
порядку.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Badge } from '@/component/ui/badge'
|
import { Badge } from '@/component/ui/badge'
|
||||||
import { Button } from '@/component/ui/button'
|
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 {
|
interface StandardsConfig {
|
||||||
registryNumber: string
|
|
||||||
sheet: string
|
sheet: string
|
||||||
startCell: string
|
startCell: string
|
||||||
maxItems: number
|
maxItems: number
|
||||||
@@ -14,51 +14,122 @@ interface StandardsPreviewProps {
|
|||||||
config: StandardsConfig
|
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> = ({
|
export const StandardsPreview: React.FC<StandardsPreviewProps> = ({
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
|
// Берем первые config.maxItems эталонов для предпросмотра
|
||||||
|
const previewStandards = mockStandards.slice(0, config.maxItems || 7)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
|
{/* Блок с измерительными эталонами */}
|
||||||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
<div>
|
||||||
<div className="space-y-3">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<Label className="text-sm font-medium">Эталоны</Label>
|
||||||
<Settings className="h-4 w-4 text-gray-500" />
|
<Button variant="outline" size="sm" disabled className="h-7 px-2">
|
||||||
<span className="text-sm font-medium">Измерительные эталоны</span>
|
<Settings className="mr-1 h-3 w-3" />
|
||||||
</div>
|
Настроить
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-xs text-gray-500">
|
<div className="space-y-1.5">
|
||||||
ГРСИ: {config.registryNumber || 'Не указан'}
|
{previewStandards.map((standard, index) => (
|
||||||
</div>
|
<div
|
||||||
<div className="text-xs text-gray-500">
|
key={standard.id}
|
||||||
Максимум: {config.maxItems} эталонов
|
className="flex items-center gap-2 rounded-md bg-muted/30 p-2 text-sm"
|
||||||
</div>
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 { ElementDefinition } from '@/entitiy/element/model/interface'
|
||||||
import { parseCellAddress } from '@/lib/cell-utils'
|
|
||||||
import { CellTarget } from '@/type/template'
|
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 { StandardsEditor } from './StandardsEditor'
|
||||||
import { StandardsPreview } from './StandardsPreview'
|
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 {
|
interface StandardsConfig {
|
||||||
registryNumber: string // ГРСИ
|
|
||||||
sheet: string // Лист Excel
|
sheet: string // Лист Excel
|
||||||
startCell: string // Первая ячейка (например "A10")
|
startCell: string // Первая ячейка (например "A10")
|
||||||
maxItems: number // Обычно 7
|
maxItems: number // Обычно 7
|
||||||
@@ -20,39 +99,125 @@ export const standardsDefinition: ElementDefinition<StandardsConfig, string[]> =
|
|||||||
icon: <Weight className="h-4 w-4" />,
|
icon: <Weight className="h-4 w-4" />,
|
||||||
version: 1,
|
version: 1,
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
registryNumber: '',
|
sheet: 'L',
|
||||||
sheet: 'Report',
|
|
||||||
startCell: 'A1',
|
startCell: 'A1',
|
||||||
maxItems: 7,
|
maxItems: 7,
|
||||||
targetCells: [],
|
targetCells: [],
|
||||||
},
|
},
|
||||||
Editor: StandardsEditor,
|
Editor: StandardsEditor,
|
||||||
Preview: StandardsPreview,
|
Preview: StandardsPreview,
|
||||||
Render: () => {
|
Render: ({ config, value, onChange }) => {
|
||||||
// Здесь нужно создать обертку для StandardsDialog
|
const [isStandardsDialogOpen, setIsStandardsDialogOpen] = useState(false)
|
||||||
// Пока возвращаем null, так как StandardsDialog требует дополнительные пропсы
|
const [selectedStandardIds, setSelectedStandardIds] = useState<string[]>(
|
||||||
return null
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Если 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 => {
|
mapToCells: cfg => {
|
||||||
const res: CellTarget[] = []
|
// Используем только ячейки, указанные пользователем в targetCells
|
||||||
const parsed = parseCellAddress(cfg.startCell)
|
return cfg.targetCells || []
|
||||||
|
},
|
||||||
|
mapToCellValues: (cfg, value) => {
|
||||||
|
// Используем targetCells из конфигурации
|
||||||
|
const cells = cfg.targetCells || []
|
||||||
|
|
||||||
if (!parsed) {
|
// Сопоставляем значения с ячейками
|
||||||
// Если не удалось распарсить, возвращаем пустой массив
|
const standardNames = Array.isArray(value) ? value : []
|
||||||
return res
|
return cells.map((target, idx) => ({
|
||||||
}
|
target,
|
||||||
|
value: standardNames[idx] || '', // Берем значение по индексу или пустую строку
|
||||||
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
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
getAvailableElementTypes,
|
getAvailableElementTypes,
|
||||||
getElementDefinition,
|
getElementDefinition,
|
||||||
} from '@/entitiy/element/model/interface'
|
} from '@/entitiy/element/model/interface'
|
||||||
|
import { useToast } from '@/lib/hooks/useToast'
|
||||||
import {
|
import {
|
||||||
autoArrangeElements,
|
autoArrangeElements,
|
||||||
findFreePosition,
|
findFreePosition,
|
||||||
@@ -14,17 +15,7 @@ import {
|
|||||||
Template,
|
Template,
|
||||||
TemplateElement,
|
TemplateElement,
|
||||||
} from '@/type/template'
|
} from '@/type/template'
|
||||||
import {
|
import { Plus, Trash2 } from 'lucide-react'
|
||||||
Calendar,
|
|
||||||
Droplets,
|
|
||||||
Edit3,
|
|
||||||
Gauge,
|
|
||||||
Plus,
|
|
||||||
Radio,
|
|
||||||
Thermometer,
|
|
||||||
Trash2,
|
|
||||||
Zap,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Button } from '../ui/button'
|
import { Button } from '../ui/button'
|
||||||
import { Checkbox } from '../ui/checkbox'
|
import { Checkbox } from '../ui/checkbox'
|
||||||
@@ -43,7 +34,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '../ui/select'
|
} from '../ui/select'
|
||||||
import { Textarea } from '../ui/textarea'
|
|
||||||
import { ElementFormulaBar } from './ElementFormulaBar'
|
import { ElementFormulaBar } from './ElementFormulaBar'
|
||||||
import { VisualLayoutEditor } from './VisualLayoutEditor'
|
import { VisualLayoutEditor } from './VisualLayoutEditor'
|
||||||
|
|
||||||
@@ -56,6 +46,8 @@ const CellTargetEditor: React.FC<{
|
|||||||
targets: CellTarget[]
|
targets: CellTarget[]
|
||||||
onChange: (targets: CellTarget[]) => void
|
onChange: (targets: CellTarget[]) => void
|
||||||
}> = ({ targets, onChange }) => {
|
}> = ({ targets, onChange }) => {
|
||||||
|
const { warning } = useToast()
|
||||||
|
|
||||||
const handleAddTarget = () => {
|
const handleAddTarget = () => {
|
||||||
onChange([...targets, { sheet: 'L', cell: '' }])
|
onChange([...targets, { sheet: 'L', cell: '' }])
|
||||||
}
|
}
|
||||||
@@ -75,6 +67,21 @@ const CellTargetEditor: React.FC<{
|
|||||||
onChange(targets.filter((_, i) => i !== index))
|
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 (
|
return (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -119,9 +126,7 @@ const CellTargetEditor: React.FC<{
|
|||||||
<Input
|
<Input
|
||||||
placeholder="A1, B5..."
|
placeholder="A1, B5..."
|
||||||
value={target.cell}
|
value={target.cell}
|
||||||
onChange={e =>
|
onChange={e => handleCellInputChange(index, e.target.value)}
|
||||||
handleUpdateTarget(index, 'cell', e.target.value)
|
|
||||||
}
|
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,163 +164,7 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({
|
|||||||
return <elementDefinition.Preview config={element as any} />
|
return <elementDefinition.Preview config={element as any} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback для старых элементов
|
// 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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
|
<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>}
|
{element.required && <span className="ml-1 text-red-500">*</span>}
|
||||||
</label>
|
</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 && (
|
{element.targetCells && element.targetCells.length > 0 && (
|
||||||
<div className="mt-2 flex flex-wrap gap-1">
|
<div className="mt-2 flex flex-wrap gap-1">
|
||||||
{element.targetCells.map((target, index) => (
|
{element.targetCells.map((target, index) => (
|
||||||
@@ -699,7 +554,6 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
showGrid: true,
|
showGrid: true,
|
||||||
}
|
}
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
|
||||||
const [editingElement, setEditingElement] = useState<TemplateElement | null>(
|
const [editingElement, setEditingElement] = useState<TemplateElement | null>(
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export const ToastContainer: FC<ToastContainerProps> = ({
|
|||||||
if (toasts.length === 0) return null
|
if (toasts.length === 0) return null
|
||||||
|
|
||||||
return (
|
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 => (
|
{toasts.map(toast => (
|
||||||
<div key={toast.id} className="pointer-events-auto">
|
<div key={toast.id} className="pointer-events-auto">
|
||||||
<ToastComponent toast={toast} onRemove={onRemove} />
|
<ToastComponent toast={toast} onRemove={onRemove} />
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ export const buttonGroupDefinition: ElementDefinition<
|
|||||||
},
|
},
|
||||||
// Берём целевые ячейки прямо из конфига, если они заданы в редакторе
|
// Берём целевые ячейки прямо из конфига, если они заданы в редакторе
|
||||||
mapToCells: cfg => cfg.targetCells || [],
|
mapToCells: cfg => cfg.targetCells || [],
|
||||||
|
mapToCellValues: (config, value) => {
|
||||||
|
const cells = config.targetCells || []
|
||||||
|
|
||||||
|
// Находим option по value чтобы получить label
|
||||||
|
const selectedOption = config.options?.find(opt => opt.value === value)
|
||||||
|
const cellValue = selectedOption ? selectedOption.label : value || ''
|
||||||
|
|
||||||
|
return cells.map(target => ({ target, value: cellValue }))
|
||||||
|
},
|
||||||
Editor: ({ config, onChange }) => {
|
Editor: ({ config, onChange }) => {
|
||||||
const handleAddOption = () => {
|
const handleAddOption = () => {
|
||||||
onChange({
|
onChange({
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export const textDefinition: ElementDefinition<TextConfig, string> = {
|
|||||||
name: '',
|
name: '',
|
||||||
},
|
},
|
||||||
mapToCells: config => config.targetCells || [],
|
mapToCells: config => config.targetCells || [],
|
||||||
|
mapToCellValues: (config, value) => {
|
||||||
|
const cells = config.targetCells || []
|
||||||
|
return cells.map(target => ({ target, value: value || '' }))
|
||||||
|
},
|
||||||
Editor: () => <ExtraSettingsBadge />,
|
Editor: () => <ExtraSettingsBadge />,
|
||||||
Preview: ({ config }) => (
|
Preview: ({ config }) => (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ export interface ElementDefinition<Config = any, Value = any> {
|
|||||||
|
|
||||||
/* бизнес-логика */
|
/* бизнес-логика */
|
||||||
defaultConfig: Config
|
defaultConfig: Config
|
||||||
mapToCells(config: Config): CellTarget[] // куда пишем данные
|
mapToCells(config: Config): CellTarget[] // куда пишем данные (для отображения)
|
||||||
|
mapToCellValues(
|
||||||
|
config: Config,
|
||||||
|
value: Value
|
||||||
|
): Array<{ target: CellTarget; value: any }> // готовые пары ячейка-значение для записи
|
||||||
|
|
||||||
/* мета */
|
/* мета */
|
||||||
version: number
|
version: number
|
||||||
|
|||||||
85
src/hook/useFileQueries.ts
Normal file
85
src/hook/useFileQueries.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import {
|
||||||
|
getFileData,
|
||||||
|
getFilesByTemplateId,
|
||||||
|
getLatestFileForTemplate,
|
||||||
|
parseExcelFile,
|
||||||
|
} from '@/service/fileApiService'
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
// Ключи для React Query
|
||||||
|
export const fileKeys = {
|
||||||
|
all: ['files'] as const,
|
||||||
|
byTemplate: (templateId: string) =>
|
||||||
|
[...fileKeys.all, 'template', templateId] as const,
|
||||||
|
latest: (templateId: string) =>
|
||||||
|
[...fileKeys.byTemplate(templateId), 'latest'] as const,
|
||||||
|
data: (fileId: string) => [...fileKeys.all, 'data', fileId] as const,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хук для получения файлов по template_id с кешированием
|
||||||
|
export function useFilesByTemplate(templateId: string | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: fileKeys.byTemplate(templateId || ''),
|
||||||
|
queryFn: () => getFilesByTemplateId(templateId!),
|
||||||
|
enabled: !!templateId,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 минут
|
||||||
|
gcTime: 10 * 60 * 1000, // 10 минут кеш
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хук для получения последнего файла для шаблона
|
||||||
|
export function useLatestFileForTemplate(templateId: string | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: fileKeys.latest(templateId || ''),
|
||||||
|
queryFn: () => getLatestFileForTemplate(templateId!),
|
||||||
|
enabled: !!templateId,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 минут
|
||||||
|
gcTime: 10 * 60 * 1000, // 10 минут кеш
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хук для получения данных файла по ID
|
||||||
|
export function useFileData(fileId: string | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: fileKeys.data(fileId || ''),
|
||||||
|
queryFn: () => getFileData(fileId!),
|
||||||
|
enabled: !!fileId,
|
||||||
|
staleTime: 10 * 60 * 1000, // 10 минут для данных файла
|
||||||
|
gcTime: 30 * 60 * 1000, // 30 минут кеш
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хук для загрузки Excel файла
|
||||||
|
export function useParseExcelFile() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ file, templateId }: { file: File; templateId: string }) =>
|
||||||
|
parseExcelFile(file, templateId),
|
||||||
|
onSuccess: (data, variables) => {
|
||||||
|
// Инвалидируем связанные кеши после успешной загрузки
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: fileKeys.byTemplate(variables.templateId),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: fileKeys.latest(variables.templateId),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Сохраняем данные файла в кеш
|
||||||
|
queryClient.setQueryData(fileKeys.data(data.fileId), data)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хук для предзагрузки данных файла
|
||||||
|
export function usePrefetchFileData() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return (fileId: string) => {
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: fileKeys.data(fileId),
|
||||||
|
queryFn: () => getFileData(fileId),
|
||||||
|
staleTime: 10 * 60 * 1000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -192,10 +192,10 @@ export function useMultiSheetEngine({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запускаем безопасный пересчет
|
// Запускаем отложенный пересчет с дебаунсом (избегаем двойного пересчёта)
|
||||||
safeRecalc()
|
debouncedRecalc()
|
||||||
},
|
},
|
||||||
[safeRecalc]
|
[debouncedRecalc]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Получение значения ячейки (вычисленное)
|
// Получение значения ячейки (вычисленное)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useDebounce } from 'use-debounce'
|
import { useDebounce } from 'use-debounce'
|
||||||
|
import type { ApiSheet, CreateSheetInput } from '../service/sheetApiService'
|
||||||
import {
|
import {
|
||||||
ApiSheet,
|
useCreateSheet,
|
||||||
createSheet,
|
usePatchSheet,
|
||||||
getSheetsByTemplate,
|
useSheetsByTemplate,
|
||||||
patchSheet,
|
} from './useSheetQueries'
|
||||||
} from '../service/sheetApiService'
|
|
||||||
|
|
||||||
export interface UseSheetAutoSaveOptions {
|
export interface UseSheetAutoSaveOptions {
|
||||||
templateId?: string
|
templateId?: string
|
||||||
@@ -19,133 +19,88 @@ export const useSheetAutoSave = (
|
|||||||
revision: number,
|
revision: number,
|
||||||
options: UseSheetAutoSaveOptions = {}
|
options: UseSheetAutoSaveOptions = {}
|
||||||
) => {
|
) => {
|
||||||
console.log('getAllModifiedCells', getAllModifiedCells())
|
|
||||||
const memoizedGetAllModifiedCells = useCallback(getAllModifiedCells, [
|
|
||||||
getAllModifiedCells,
|
|
||||||
])
|
|
||||||
|
|
||||||
const { templateId, autoSaveDelay = 2000, enableAutoSave = true } = options
|
const { templateId, autoSaveDelay = 2000, enableAutoSave = true } = options
|
||||||
|
|
||||||
// Состояние
|
// Состояние
|
||||||
const [lastSavedRevision, setLastSavedRevision] = useState(0)
|
const [lastSavedRevision, setLastSavedRevision] = useState(0)
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
|
||||||
const [lastSaveError, setLastSaveError] = useState<string | null>(null)
|
const [lastSaveError, setLastSaveError] = useState<string | null>(null)
|
||||||
const [sheets, setSheets] = useState<Record<string, ApiSheet>>({})
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
// React Query хуки
|
||||||
|
const {
|
||||||
|
data: sheetsResponse,
|
||||||
|
isLoading,
|
||||||
|
error: loadError,
|
||||||
|
refetch: refetchSheets,
|
||||||
|
} = useSheetsByTemplate(templateId)
|
||||||
|
|
||||||
|
const createSheetMutation = useCreateSheet()
|
||||||
|
const patchSheetMutation = usePatchSheet()
|
||||||
|
|
||||||
|
// Мемоизируем карту листов
|
||||||
|
const sheetsMap = useMemo(() => {
|
||||||
|
if (!sheetsResponse?.sheets) return {}
|
||||||
|
|
||||||
|
return sheetsResponse.sheets.reduce<Record<string, ApiSheet>>(
|
||||||
|
(acc, sheet) => {
|
||||||
|
acc[sheet.name] = sheet
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}, [sheetsResponse?.sheets])
|
||||||
|
|
||||||
// Дебаунсинг ревизии для автосохранения
|
// Дебаунсинг ревизии для автосохранения
|
||||||
const [debouncedRevision] = useDebounce(revision, autoSaveDelay)
|
const [debouncedRevision] = useDebounce(revision, autoSaveDelay)
|
||||||
|
|
||||||
// Загрузка листов для шаблона при инициализации
|
// Состояние сохранения
|
||||||
const loadSheets = useCallback(async () => {
|
const isSaving = createSheetMutation.isPending || patchSheetMutation.isPending
|
||||||
if (!templateId) return {}
|
|
||||||
|
|
||||||
setIsLoading(true)
|
// Обновляем ошибку при ошибках загрузки или сохранения
|
||||||
try {
|
|
||||||
const response = await getSheetsByTemplate(templateId)
|
|
||||||
|
|
||||||
const sheetsMap = response.sheets.reduce<Record<string, ApiSheet>>(
|
|
||||||
(acc, sheet) => {
|
|
||||||
const existingSheet = acc[sheet.name]
|
|
||||||
|
|
||||||
if (existingSheet) {
|
|
||||||
// Если лист уже есть, объединяем ячейки
|
|
||||||
acc[sheet.name] = {
|
|
||||||
...sheet, // берем актуальную версию листа
|
|
||||||
cells: {
|
|
||||||
...existingSheet.cells, // старые ячейки
|
|
||||||
...sheet.cells, // новые заменяют старые с тем же ключом
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
acc[sheet.name] = { ...sheet }
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
setSheets(sheetsMap)
|
|
||||||
return sheetsMap
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Ошибка при загрузке листов:', error)
|
|
||||||
setLastSaveError('Ошибка загрузки листов')
|
|
||||||
return {}
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}, [templateId])
|
|
||||||
|
|
||||||
// Загрузка листов при изменении templateId
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (templateId) {
|
if (loadError) {
|
||||||
loadSheets()
|
setLastSaveError('Ошибка загрузки листов')
|
||||||
|
} else if (createSheetMutation.error) {
|
||||||
|
setLastSaveError(createSheetMutation.error.message)
|
||||||
|
} else if (patchSheetMutation.error) {
|
||||||
|
setLastSaveError(patchSheetMutation.error.message)
|
||||||
}
|
}
|
||||||
}, [templateId, loadSheets])
|
}, [loadError, createSheetMutation.error, patchSheetMutation.error])
|
||||||
|
|
||||||
// Основная функция сохранения через API
|
// Основная функция сохранения через React Query
|
||||||
const performSave = useCallback(
|
const performSave = useCallback(
|
||||||
async (data: Record<string, Record<string, any>>) => {
|
async (data: Record<string, Record<string, any>>) => {
|
||||||
if (!templateId) return
|
if (!templateId) return
|
||||||
|
|
||||||
// Получаем список всех листов, которые нужно обработать:
|
// Получаем список всех листов, которые нужно обработать
|
||||||
// 1. Листы с данными (из data)
|
|
||||||
// 2. Существующие листы, которые могли быть очищены
|
|
||||||
const dataSheetNames = Object.keys(data)
|
const dataSheetNames = Object.keys(data)
|
||||||
const existingSheetNames = Object.keys(sheets)
|
const existingSheetNames = Object.keys(sheetsMap)
|
||||||
const allSheetNames = new Set([...dataSheetNames, ...existingSheetNames])
|
const allSheetNames = new Set([...dataSheetNames, ...existingSheetNames])
|
||||||
|
|
||||||
if (allSheetNames.size === 0) return
|
if (allSheetNames.size === 0) return
|
||||||
|
|
||||||
setIsSaving(true)
|
|
||||||
setLastSaveError(null)
|
setLastSaveError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Создаем массив промисов для параллельного выполнения
|
||||||
const savePromises = Array.from(allSheetNames).map(async sheetName => {
|
const savePromises = Array.from(allSheetNames).map(async sheetName => {
|
||||||
const sheetCells = data[sheetName] || {} // если данных нет, используем пустой объект
|
const sheetCells = data[sheetName] || {} // если данных нет, используем пустой объект
|
||||||
const existingSheet = sheets[sheetName]
|
const existingSheet = sheetsMap[sheetName]
|
||||||
|
|
||||||
if (existingSheet) {
|
if (existingSheet) {
|
||||||
// Всегда сохраняем, даже если ячейки пустые (очистка листа)
|
// Обновляем существующий лист
|
||||||
await patchSheet(existingSheet.id, {
|
await patchSheetMutation.mutateAsync({
|
||||||
cells: sheetCells,
|
sheetId: existingSheet.id,
|
||||||
|
data: { cells: sheetCells },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Обновляем локальное состояние
|
|
||||||
setSheets(prev => ({
|
|
||||||
...prev,
|
|
||||||
[sheetName]: {
|
|
||||||
...existingSheet,
|
|
||||||
cells: sheetCells,
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
} else {
|
} else {
|
||||||
// Создаем новый лист только если есть данные
|
// Создаем новый лист только если есть данные
|
||||||
if (Object.keys(sheetCells).length > 0) {
|
if (Object.keys(sheetCells).length > 0) {
|
||||||
const newSheet = await createSheet({
|
const createInput: CreateSheetInput = {
|
||||||
name: sheetName,
|
name: sheetName,
|
||||||
cells: sheetCells,
|
cells: sheetCells,
|
||||||
template_id: templateId,
|
template_id: templateId,
|
||||||
})
|
|
||||||
|
|
||||||
// Получаем полную информацию о созданном листе
|
|
||||||
const fullSheetData: ApiSheet = {
|
|
||||||
id: newSheet.id,
|
|
||||||
name: sheetName,
|
|
||||||
cells: sheetCells,
|
|
||||||
template_id: templateId,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
deleted_at: null,
|
|
||||||
}
|
}
|
||||||
|
await createSheetMutation.mutateAsync(createInput)
|
||||||
// Обновляем локальное состояние
|
|
||||||
setSheets(prev => ({
|
|
||||||
...prev,
|
|
||||||
[sheetName]: fullSheetData,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -157,11 +112,9 @@ export const useSheetAutoSave = (
|
|||||||
error instanceof Error ? error.message : 'Неизвестная ошибка'
|
error instanceof Error ? error.message : 'Неизвестная ошибка'
|
||||||
setLastSaveError(errorMessage)
|
setLastSaveError(errorMessage)
|
||||||
console.error('❌ Ошибка при сохранении листов:', errorMessage)
|
console.error('❌ Ошибка при сохранении листов:', errorMessage)
|
||||||
} finally {
|
|
||||||
setIsSaving(false)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[templateId, sheets, revision]
|
[templateId, sheetsMap, revision, createSheetMutation, patchSheetMutation]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Автосохранение при изменении дебаунсированной ревизии
|
// Автосохранение при изменении дебаунсированной ревизии
|
||||||
@@ -174,8 +127,16 @@ export const useSheetAutoSave = (
|
|||||||
templateId
|
templateId
|
||||||
|
|
||||||
if (shouldAutoSave) {
|
if (shouldAutoSave) {
|
||||||
console.log('✅ Выполняем автосохранение для ревизии:', debouncedRevision)
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
performSave(memoizedGetAllModifiedCells())
|
console.log(
|
||||||
|
'✅ Выполняем автосохранение для ревизии:',
|
||||||
|
debouncedRevision
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const diff = getAllModifiedCells()
|
||||||
|
if (Object.keys(diff).length > 0) {
|
||||||
|
performSave(diff)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
enableAutoSave,
|
enableAutoSave,
|
||||||
@@ -184,31 +145,44 @@ export const useSheetAutoSave = (
|
|||||||
isCalculating,
|
isCalculating,
|
||||||
isSaving,
|
isSaving,
|
||||||
templateId,
|
templateId,
|
||||||
memoizedGetAllModifiedCells,
|
getAllModifiedCells,
|
||||||
performSave,
|
performSave,
|
||||||
])
|
])
|
||||||
|
|
||||||
// Ручное сохранение
|
// Ручное сохранение
|
||||||
const manualSave = useCallback(() => {
|
const manualSave = useCallback(() => {
|
||||||
const currentData = memoizedGetAllModifiedCells()
|
const currentData = getAllModifiedCells()
|
||||||
performSave(currentData)
|
if (Object.keys(currentData).length > 0) {
|
||||||
}, [memoizedGetAllModifiedCells, performSave])
|
performSave(currentData)
|
||||||
|
}
|
||||||
|
}, [getAllModifiedCells, performSave])
|
||||||
|
|
||||||
// Функция загрузки сохраненных листов
|
// Функция загрузки сохраненных листов (теперь используем React Query кеш)
|
||||||
const loadSavedSheets = useCallback((): Record<
|
const loadSavedSheets = useCallback((): Record<
|
||||||
string,
|
string,
|
||||||
Record<string, any>
|
Record<string, any>
|
||||||
> => {
|
> => {
|
||||||
const result: Record<string, Record<string, any>> = {}
|
const result: Record<string, Record<string, any>> = {}
|
||||||
|
|
||||||
Object.entries(sheets).forEach(([sheetName, sheet]) => {
|
Object.entries(sheetsMap).forEach(([sheetName, sheet]) => {
|
||||||
if (sheet.cells && Object.keys(sheet.cells).length > 0) {
|
if (sheet.cells && Object.keys(sheet.cells).length > 0) {
|
||||||
result[sheetName] = sheet.cells
|
result[sheetName] = sheet.cells
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}, [sheets])
|
}, [sheetsMap])
|
||||||
|
|
||||||
|
// Функция загрузки листов (используем refetch из React Query)
|
||||||
|
const loadSheets = useCallback(async () => {
|
||||||
|
const result = await refetchSheets()
|
||||||
|
return (
|
||||||
|
result.data?.sheets.reduce<Record<string, ApiSheet>>((acc, sheet) => {
|
||||||
|
acc[sheet.name] = sheet
|
||||||
|
return acc
|
||||||
|
}, {}) || {}
|
||||||
|
)
|
||||||
|
}, [refetchSheets])
|
||||||
|
|
||||||
// Вычисляемые значения
|
// Вычисляемые значения
|
||||||
const isDataChanged = revision > lastSavedRevision
|
const isDataChanged = revision > lastSavedRevision
|
||||||
@@ -222,7 +196,7 @@ export const useSheetAutoSave = (
|
|||||||
hasUnsavedChanges,
|
hasUnsavedChanges,
|
||||||
lastSaveError,
|
lastSaveError,
|
||||||
lastSavedRevision,
|
lastSavedRevision,
|
||||||
sheets,
|
sheets: sheetsMap,
|
||||||
|
|
||||||
// Методы
|
// Методы
|
||||||
manualSave,
|
manualSave,
|
||||||
@@ -230,7 +204,7 @@ export const useSheetAutoSave = (
|
|||||||
loadSheets,
|
loadSheets,
|
||||||
|
|
||||||
// Утилиты
|
// Утилиты
|
||||||
getCurrentData: memoizedGetAllModifiedCells,
|
getCurrentData: getAllModifiedCells,
|
||||||
clearError: () => setLastSaveError(null),
|
clearError: () => setLastSaveError(null),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
196
src/hook/useSheetQueries.ts
Normal file
196
src/hook/useSheetQueries.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import {
|
||||||
|
createSheet,
|
||||||
|
getSheet,
|
||||||
|
getSheets,
|
||||||
|
getSheetsByTemplate,
|
||||||
|
patchSheet,
|
||||||
|
type ApiSheet,
|
||||||
|
type GetSheetsOutput,
|
||||||
|
type PatchSheetInput,
|
||||||
|
} from '@/service/sheetApiService'
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
// Ключи для React Query
|
||||||
|
export const sheetKeys = {
|
||||||
|
all: ['sheets'] as const,
|
||||||
|
byTemplate: (templateId: string) =>
|
||||||
|
[...sheetKeys.all, 'template', templateId] as const,
|
||||||
|
detail: (sheetId: string) => [...sheetKeys.all, 'detail', sheetId] as const,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хук для получения всех листов
|
||||||
|
export function useSheets() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: sheetKeys.all,
|
||||||
|
queryFn: getSheets,
|
||||||
|
staleTime: 2 * 60 * 1000, // 2 минуты
|
||||||
|
gcTime: 10 * 60 * 1000, // 10 минут кеш
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хук для получения листов по template_id с кешированием
|
||||||
|
export function useSheetsByTemplate(templateId: string | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: sheetKeys.byTemplate(templateId || ''),
|
||||||
|
queryFn: () => getSheetsByTemplate(templateId!),
|
||||||
|
enabled: !!templateId,
|
||||||
|
staleTime: 1 * 60 * 1000, // 1 минута для активных данных
|
||||||
|
gcTime: 5 * 60 * 1000, // 5 минут кеш
|
||||||
|
refetchOnWindowFocus: false, // Не перезапрашивать при фокусе окна
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хук для получения конкретного листа
|
||||||
|
export function useSheet(sheetId: string | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: sheetKeys.detail(sheetId || ''),
|
||||||
|
queryFn: () => getSheet(sheetId!),
|
||||||
|
enabled: !!sheetId,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 минут
|
||||||
|
gcTime: 10 * 60 * 1000, // 10 минут кеш
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хук для создания листа
|
||||||
|
export function useCreateSheet() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: createSheet,
|
||||||
|
onSuccess: (data, variables) => {
|
||||||
|
// Инвалидируем кеши после создания листа
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: sheetKeys.byTemplate(variables.template_id),
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: sheetKeys.all,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Добавляем новый лист в кеш списка листов для шаблона
|
||||||
|
queryClient.setQueryData<GetSheetsOutput>(
|
||||||
|
sheetKeys.byTemplate(variables.template_id),
|
||||||
|
oldData => {
|
||||||
|
if (!oldData) return { sheets: [] }
|
||||||
|
|
||||||
|
const newSheet: ApiSheet = {
|
||||||
|
id: data.id,
|
||||||
|
name: variables.name,
|
||||||
|
cells: variables.cells,
|
||||||
|
template_id: variables.template_id,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sheets: [...oldData.sheets, newSheet],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хук для обновления листа
|
||||||
|
export function usePatchSheet() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
sheetId,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
sheetId: string
|
||||||
|
data: PatchSheetInput
|
||||||
|
}) => patchSheet(sheetId, data),
|
||||||
|
onSuccess: (result, variables) => {
|
||||||
|
// Обновляем конкретный лист в кеше
|
||||||
|
queryClient.setQueryData(
|
||||||
|
sheetKeys.detail(variables.sheetId),
|
||||||
|
(oldData: any) => {
|
||||||
|
if (!oldData?.sheet) return oldData
|
||||||
|
|
||||||
|
return {
|
||||||
|
sheet: {
|
||||||
|
...oldData.sheet,
|
||||||
|
...variables.data,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Обновляем лист в списке листов для шаблона
|
||||||
|
const sheetDetail = queryClient.getQueryData(
|
||||||
|
sheetKeys.detail(variables.sheetId)
|
||||||
|
) as any
|
||||||
|
const templateId = sheetDetail?.sheet?.template_id
|
||||||
|
|
||||||
|
if (templateId) {
|
||||||
|
queryClient.setQueryData<GetSheetsOutput>(
|
||||||
|
sheetKeys.byTemplate(templateId),
|
||||||
|
oldData => {
|
||||||
|
if (!oldData) return oldData
|
||||||
|
|
||||||
|
return {
|
||||||
|
sheets: oldData.sheets.map(sheet =>
|
||||||
|
sheet.id === variables.sheetId
|
||||||
|
? {
|
||||||
|
...sheet,
|
||||||
|
name: variables.data.name || sheet.name,
|
||||||
|
cells: variables.data.cells || sheet.cells,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
: sheet
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хук для массового обновления листов (для автосохранения)
|
||||||
|
export function useBatchUpdateSheets() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (
|
||||||
|
updates: Array<{
|
||||||
|
sheetId: string
|
||||||
|
data: PatchSheetInput
|
||||||
|
templateId: string
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
// Выполняем все обновления параллельно
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
updates.map(({ sheetId, data }) => patchSheet(sheetId, data))
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
},
|
||||||
|
onSuccess: (results, variables) => {
|
||||||
|
// Обновляем кеши для всех затронутых шаблонов
|
||||||
|
const templateIds = new Set(variables.map(v => v.templateId))
|
||||||
|
|
||||||
|
templateIds.forEach(templateId => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: sheetKeys.byTemplate(templateId),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хук для предзагрузки листов шаблона
|
||||||
|
export function usePrefetchSheetsByTemplate() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return (templateId: string) => {
|
||||||
|
queryClient.prefetchQuery({
|
||||||
|
queryKey: sheetKeys.byTemplate(templateId),
|
||||||
|
queryFn: () => getSheetsByTemplate(templateId),
|
||||||
|
staleTime: 1 * 60 * 1000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ export interface ExcelFunction {
|
|||||||
export const VOLATILE_FUNCTIONS = new Set([
|
export const VOLATILE_FUNCTIONS = new Set([
|
||||||
'СЛУЧ',
|
'СЛУЧ',
|
||||||
'СЛУЧМЕЖДУ',
|
'СЛУЧМЕЖДУ',
|
||||||
|
'СЛУЧДР',
|
||||||
'СЕЙЧАС',
|
'СЕЙЧАС',
|
||||||
'СЕГОДНЯ',
|
'СЕГОДНЯ',
|
||||||
])
|
])
|
||||||
@@ -16,6 +17,44 @@ export const DEFAULT_FUNCTIONS: Record<string, ExcelFunction> = {
|
|||||||
СЛУЧ: () => Math.random(),
|
СЛУЧ: () => Math.random(),
|
||||||
СЛУЧМЕЖДУ: (низ: number, верх: number) =>
|
СЛУЧМЕЖДУ: (низ: number, верх: number) =>
|
||||||
Math.floor(Math.random() * (верх - низ + 1)) + низ,
|
Math.floor(Math.random() * (верх - низ + 1)) + низ,
|
||||||
|
СЛУЧДР: (низ: number, верх: number, знаков: number = 0) => {
|
||||||
|
// Проверка валидности входных параметров
|
||||||
|
if (
|
||||||
|
typeof низ !== 'number' ||
|
||||||
|
typeof верх !== 'number' ||
|
||||||
|
typeof знаков !== 'number'
|
||||||
|
) {
|
||||||
|
return '#ЗНАЧ!' // Ошибка значения
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFinite(низ) || !isFinite(верх) || !isFinite(знаков)) {
|
||||||
|
return '#ЧИСЛО!' // Ошибка числа
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если низ больше верха, меняем местами
|
||||||
|
if (низ > верх) {
|
||||||
|
;[низ, верх] = [верх, низ]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если низ равен верху, возвращаем это значение
|
||||||
|
if (низ === верх) {
|
||||||
|
const множитель = Math.pow(10, Math.max(0, Math.floor(знаков)))
|
||||||
|
return Math.round(низ * множитель) / множитель
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ограничиваем количество знаков после запятой (максимум 15 для точности JavaScript)
|
||||||
|
const ограниченныеЗнаки = Math.max(0, Math.min(15, Math.floor(знаков)))
|
||||||
|
|
||||||
|
// Генерируем случайное число
|
||||||
|
const случайное = Math.random() * (верх - низ) + низ
|
||||||
|
const множитель = Math.pow(10, ограниченныеЗнаки)
|
||||||
|
|
||||||
|
// Округляем и возвращаем результат
|
||||||
|
const результат = Math.round(случайное * множитель) / множитель
|
||||||
|
|
||||||
|
// Проверяем, что результат в допустимых пределах
|
||||||
|
return Math.max(низ, Math.min(верх, результат))
|
||||||
|
},
|
||||||
СУММ: (...args: any[]) => {
|
СУММ: (...args: any[]) => {
|
||||||
const flatArgs = args.flat()
|
const flatArgs = args.flat()
|
||||||
return flatArgs.reduce(
|
return flatArgs.reduce(
|
||||||
|
|||||||
@@ -1,105 +1,15 @@
|
|||||||
import DualSpreadsheet from '@/component/DualSpreadsheet/DualSpreadsheet'
|
import DualSpreadsheet from '@/component/DualSpreadsheet/DualSpreadsheet'
|
||||||
import { StandardsSelector } from '@/component/StandardsElement/StandardsSelector'
|
|
||||||
import { Badge } from '@/component/ui/badge'
|
|
||||||
import { Button } from '@/component/ui/button'
|
import { Button } from '@/component/ui/button'
|
||||||
import { Checkbox } from '@/component/ui/checkbox'
|
|
||||||
import { Input } from '@/component/ui/input'
|
|
||||||
import { Label } from '@/component/ui/label'
|
|
||||||
import { RadioGroup, RadioGroupItem } from '@/component/ui/radio-group'
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/component/ui/select'
|
|
||||||
import { Textarea } from '@/component/ui/textarea'
|
|
||||||
import { getElementDefinition } from '@/entitiy/element/model/interface'
|
import { getElementDefinition } from '@/entitiy/element/model/interface'
|
||||||
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
||||||
import { cellAddressToCoordinates } from '@/lib/cell-utils'
|
import { cellAddressToCoordinates } from '@/lib/cell-utils'
|
||||||
import { useToast } from '@/lib/hooks/useToast'
|
import { useToast } from '@/lib/hooks/useToast'
|
||||||
import { getLatestFileForTemplate } from '@/service/fileApiService'
|
import { getLatestFileForTemplate } from '@/service/fileApiService'
|
||||||
import { Template, TemplateElement } from '@/type/template'
|
import { Template, TemplateElement } from '@/type/template'
|
||||||
import { ArrowLeft, FileText, Grid, Save, Settings, Wrench } from 'lucide-react'
|
import { ArrowLeft, FileText, Grid, Save } from 'lucide-react'
|
||||||
import { FC, useEffect, useMemo, useRef, useState } from 'react'
|
import { FC, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
// Моковые данные для эталонов (такие же как в StandardsSelector)
|
|
||||||
const mockStandards = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'Эталон массы 1 кг',
|
|
||||||
shortName: 'ЭМ-1кг',
|
|
||||||
type: 'Масса',
|
|
||||||
registryNumber: 'ГРСИ 12345-01',
|
|
||||||
range: '0.5-2 кг',
|
|
||||||
accuracy: '±0.001 г',
|
|
||||||
certificateNumber: 'СИ-2024-001',
|
|
||||||
validUntil: '2025-12-31',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'Эталон длины 1 м',
|
|
||||||
shortName: 'ЭД-1м',
|
|
||||||
type: 'Длина',
|
|
||||||
registryNumber: 'ГРСИ 12345-02',
|
|
||||||
range: '0.5-2 м',
|
|
||||||
accuracy: '±0.001 мм',
|
|
||||||
certificateNumber: 'СИ-2024-002',
|
|
||||||
validUntil: '2025-06-30',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'Эталон температуры 20°C',
|
|
||||||
shortName: 'ЭТ-20°C',
|
|
||||||
type: 'Температура',
|
|
||||||
registryNumber: 'ГРСИ 12345-03',
|
|
||||||
range: '15-25°C',
|
|
||||||
accuracy: '±0.01°C',
|
|
||||||
certificateNumber: 'СИ-2024-003',
|
|
||||||
validUntil: '2025-03-15',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
name: 'Эталон давления 1 Па',
|
|
||||||
shortName: 'ЭД-1Па',
|
|
||||||
type: 'Давление',
|
|
||||||
registryNumber: 'ГРСИ 12345-04',
|
|
||||||
range: '0.5-2 Па',
|
|
||||||
accuracy: '±0.001 Па',
|
|
||||||
certificateNumber: 'СИ-2024-004',
|
|
||||||
validUntil: '2025-09-20',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
name: 'Эталон времени 1 с',
|
|
||||||
shortName: 'ЭВ-1с',
|
|
||||||
type: 'Время',
|
|
||||||
registryNumber: 'ГРСИ 12345-05',
|
|
||||||
range: '0.5-2 с',
|
|
||||||
accuracy: '±0.000001 с',
|
|
||||||
certificateNumber: 'СИ-2024-005',
|
|
||||||
validUntil: '2025-12-01',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// Функция для получения данных эталона по ID
|
|
||||||
const getStandardById = (id: string) => {
|
|
||||||
return mockStandards.find(standard => standard.id === id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для получения типа эталона в сокращенном виде
|
|
||||||
const getStandardTypeBadge = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'Манометр грузопоршневой':
|
|
||||||
return 'МГП'
|
|
||||||
case 'Калибратор давления':
|
|
||||||
return 'КД'
|
|
||||||
default:
|
|
||||||
return 'МО'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProtocolFormProps {
|
interface ProtocolFormProps {
|
||||||
template: Template
|
template: Template
|
||||||
onSave: (data: Record<string, any>) => void
|
onSave: (data: Record<string, any>) => void
|
||||||
@@ -113,297 +23,52 @@ interface FormElementProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
|
const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
|
||||||
const [isStandardsDialogOpen, setIsStandardsDialogOpen] = useState(false)
|
// Получаем определение элемента из реестра
|
||||||
|
const elementDefinition = getElementDefinition(element.type)
|
||||||
|
|
||||||
const renderInput = () => {
|
// Если определение элемента не найдено, показываем ошибку
|
||||||
switch (element.type) {
|
if (!elementDefinition) {
|
||||||
case 'text':
|
return (
|
||||||
// Используем определение элемента из реестра для рендеринга
|
<div className="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700">
|
||||||
const textDefinition = getElementDefinition('text')
|
Элемент типа "{element.type}" не найден в реестре
|
||||||
if (textDefinition && textDefinition.Render) {
|
</div>
|
||||||
return (
|
)
|
||||||
<textDefinition.Render
|
|
||||||
config={element as any}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
placeholder={element.placeholder}
|
|
||||||
value={value || ''}
|
|
||||||
onChange={e => onChange(e.target.value)}
|
|
||||||
required={element.required}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'textarea':
|
|
||||||
return (
|
|
||||||
<Textarea
|
|
||||||
placeholder={element.placeholder}
|
|
||||||
value={value || ''}
|
|
||||||
onChange={e => onChange(e.target.value)}
|
|
||||||
required={element.required}
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'number':
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
placeholder={element.placeholder}
|
|
||||||
value={value || ''}
|
|
||||||
onChange={e =>
|
|
||||||
onChange(e.target.value ? parseFloat(e.target.value) : '')
|
|
||||||
}
|
|
||||||
required={element.required}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'date':
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={value || ''}
|
|
||||||
onChange={e => onChange(e.target.value)}
|
|
||||||
required={element.required}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'select':
|
|
||||||
return (
|
|
||||||
<Select value={value || ''} onValueChange={onChange}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={element.placeholder || 'Выберите значение'}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{element.options?.map(option => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'radio':
|
|
||||||
return (
|
|
||||||
<RadioGroup value={value || ''} onValueChange={onChange}>
|
|
||||||
{element.options?.map(option => (
|
|
||||||
<div key={option.value} className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem
|
|
||||||
value={option.value}
|
|
||||||
id={`${element.id}-${option.value}`}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor={`${element.id}-${option.value}`}
|
|
||||||
className="text-sm font-medium"
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'checkbox':
|
|
||||||
return (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
checked={!!value}
|
|
||||||
onCheckedChange={onChange}
|
|
||||||
id={element.id}
|
|
||||||
/>
|
|
||||||
<label htmlFor={element.id} className="text-sm font-medium">
|
|
||||||
{element.placeholder || 'Отметить'}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'standards':
|
|
||||||
const selectedStandards = Array.isArray(value) ? value : []
|
|
||||||
const selectedStandardsData = selectedStandards
|
|
||||||
.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={selectedStandards}
|
|
||||||
onChange={onChange}
|
|
||||||
registryNumber={
|
|
||||||
element.targetCells?.[0]?.displayName || 'ГРСИ 12345'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'calibration_conditions':
|
|
||||||
// Используем определение элемента из реестра для рендеринга
|
|
||||||
const elementDefinition = getElementDefinition('calibration_conditions')
|
|
||||||
if (elementDefinition && elementDefinition.Render) {
|
|
||||||
return (
|
|
||||||
<elementDefinition.Render
|
|
||||||
config={element as any}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
|
|
||||||
case 'button_group':
|
|
||||||
// Используем определение элемента из реестра для рендеринга
|
|
||||||
const buttonGroupDefinition = getElementDefinition('button_group')
|
|
||||||
if (buttonGroupDefinition && buttonGroupDefinition.Render) {
|
|
||||||
return (
|
|
||||||
<buttonGroupDefinition.Render
|
|
||||||
config={element as any}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, использует ли элемент определение из реестра для рендеринга
|
const renderElement = () => (
|
||||||
const elementDefinition = getElementDefinition(element.type)
|
<elementDefinition.Render
|
||||||
const usesElementRender =
|
config={element as any}
|
||||||
elementDefinition &&
|
value={value}
|
||||||
elementDefinition.Render &&
|
onChange={onChange}
|
||||||
(element.type === 'text' ||
|
/>
|
||||||
element.type === 'calibration_conditions' ||
|
)
|
||||||
element.type === 'button_group')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* Показываем label и индикатор ячеек только если элемент не использует свой Render компонент */}
|
{/* Показываем индикатор ячеек для всех элементов */}
|
||||||
{!usesElementRender && (
|
{element.targetCells && element.targetCells.length > 0 && (
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex justify-end">
|
||||||
<div className="flex items-center gap-1">
|
<div className="group/cells relative">
|
||||||
<label className="text-sm font-medium text-gray-900">
|
<Grid className="h-3 w-3 cursor-help text-gray-500 opacity-60 transition-opacity hover:opacity-100" />
|
||||||
{element.label}
|
<div className="pointer-events-none absolute right-0 top-full z-50 mt-2 whitespace-nowrap rounded bg-gray-900 p-2 text-xs text-white opacity-0 shadow-lg transition-opacity group-hover/cells:opacity-100">
|
||||||
</label>
|
<div className="mb-1 font-medium">Целевые ячейки:</div>
|
||||||
{element.required && (
|
{element.targetCells.map((cell, i) => (
|
||||||
<span className="-mt-1.5 inline-block h-1 w-1 rounded-full bg-red-500" />
|
<div key={i} className="font-mono">
|
||||||
)}
|
{cell.sheet}!{cell.cell}
|
||||||
</div>
|
{cell.displayName && (
|
||||||
{/* Индикатор ячеек */}
|
<span className="ml-1 text-gray-300">
|
||||||
{element.targetCells && element.targetCells.length > 0 && (
|
({cell.displayName})
|
||||||
<div className="group/cells relative">
|
</span>
|
||||||
<Grid className="h-3 w-3 cursor-help text-gray-500 opacity-60 transition-opacity hover:opacity-100" />
|
)}
|
||||||
<div className="pointer-events-none absolute right-0 top-full z-50 mt-2 whitespace-nowrap rounded bg-gray-900 p-2 text-xs text-white opacity-0 shadow-lg transition-opacity group-hover/cells:opacity-100">
|
</div>
|
||||||
<div className="mb-1 font-medium">Целевые ячейки:</div>
|
))}
|
||||||
{element.targetCells.map((cell, i) => (
|
|
||||||
<div key={i} className="font-mono">
|
|
||||||
{cell.sheet}!{cell.cell}
|
|
||||||
{cell.displayName && (
|
|
||||||
<span className="ml-1 text-gray-300">
|
|
||||||
({cell.displayName})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Для элементов с собственными Render компонентами показываем только индикатор ячеек */}
|
{/* Рендер элемента через единый интерфейс */}
|
||||||
{usesElementRender &&
|
{renderElement()}
|
||||||
element.targetCells &&
|
|
||||||
element.targetCells.length > 0 && (
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<div className="group/cells relative">
|
|
||||||
<Grid className="h-3 w-3 cursor-help text-gray-500 opacity-60 transition-opacity hover:opacity-100" />
|
|
||||||
<div className="pointer-events-none absolute right-0 top-full z-50 mt-2 whitespace-nowrap rounded bg-gray-900 p-2 text-xs text-white opacity-0 shadow-lg transition-opacity group-hover/cells:opacity-100">
|
|
||||||
<div className="mb-1 font-medium">Целевые ячейки:</div>
|
|
||||||
{element.targetCells.map((cell, i) => (
|
|
||||||
<div key={i} className="font-mono">
|
|
||||||
{cell.sheet}!{cell.cell}
|
|
||||||
{cell.displayName && (
|
|
||||||
<span className="ml-1 text-gray-300">
|
|
||||||
({cell.displayName})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{renderInput()}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -423,54 +88,29 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
|||||||
// Прямо обновляем движок, чтобы формулы пересчитались
|
// Прямо обновляем движок, чтобы формулы пересчитались
|
||||||
if (engineRef.current) {
|
if (engineRef.current) {
|
||||||
const element = template.elements.find(el => el.id === elementId)
|
const element = template.elements.find(el => el.id === elementId)
|
||||||
if (element && element.targetCells) {
|
if (element) {
|
||||||
const cells = element.targetCells
|
// Получаем определение элемента
|
||||||
|
const elementDefinition = getElementDefinition(element.type)
|
||||||
cells.forEach((tc, idx) => {
|
if (elementDefinition) {
|
||||||
let cellValue: any = value
|
// Используем единый интерфейс для получения пар ячейка-значение
|
||||||
|
const cellValues = elementDefinition.mapToCellValues(
|
||||||
// Специальная обработка для массивов эталонов
|
element as any,
|
||||||
if (element.type === 'standards' && Array.isArray(value)) {
|
value
|
||||||
cellValue = getStandardById(value[idx] as string)?.name || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// Специальная обработка для условий калибровки
|
|
||||||
if (
|
|
||||||
element.type === 'calibration_conditions' &&
|
|
||||||
typeof value === 'object' &&
|
|
||||||
value !== null
|
|
||||||
) {
|
|
||||||
const propsOrder = [
|
|
||||||
'temperature',
|
|
||||||
'humidity',
|
|
||||||
'pressure',
|
|
||||||
'voltage',
|
|
||||||
'frequency',
|
|
||||||
'lastUpdated',
|
|
||||||
] as const
|
|
||||||
cellValue = (value as any)[propsOrder[idx]] ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// Специальная обработка для группы кнопок
|
|
||||||
if (element.type === 'button_group' && typeof value === 'string') {
|
|
||||||
// Найдем соответствующий option для получения label
|
|
||||||
const buttonOption = element.options?.find(
|
|
||||||
opt => opt.value === value
|
|
||||||
)
|
|
||||||
cellValue = buttonOption ? buttonOption.label : value
|
|
||||||
}
|
|
||||||
|
|
||||||
const { row, col } = cellAddressToCoordinates(tc.cell)
|
|
||||||
const sheetName =
|
|
||||||
tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
|
|
||||||
engineRef.current.setCellValueWithoutRecalc(
|
|
||||||
sheetName,
|
|
||||||
row,
|
|
||||||
col,
|
|
||||||
cellValue
|
|
||||||
)
|
)
|
||||||
})
|
|
||||||
engineRef.current.debouncedRecalc()
|
// Записываем все значения в соответствующие ячейки
|
||||||
|
cellValues.forEach(({ target, value: cellValue }) => {
|
||||||
|
const { row, col } = cellAddressToCoordinates(target.cell)
|
||||||
|
engineRef.current.setCellValueWithoutRecalc(
|
||||||
|
target.sheet,
|
||||||
|
row,
|
||||||
|
col,
|
||||||
|
cellValue
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
engineRef.current.debouncedRecalc()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -503,28 +143,22 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
|||||||
let value = formData[el.id]
|
let value = formData[el.id]
|
||||||
if (value === undefined || value === null) value = ''
|
if (value === undefined || value === null) value = ''
|
||||||
|
|
||||||
let cells =
|
// Получаем определение элемента
|
||||||
el.targetCells && el.targetCells.length > 0
|
const elementDefinition = getElementDefinition(el.type)
|
||||||
? el.targetCells
|
if (elementDefinition) {
|
||||||
: undefined
|
// Используем единый интерфейс для получения пар ячейка-значение
|
||||||
if ((!cells || cells.length === 0) && el.type) {
|
const cellValues = elementDefinition.mapToCellValues(el as any, value)
|
||||||
const def = getElementDefinition(el.type as any)
|
|
||||||
if (def?.mapToCells) cells = def.mapToCells(el as any)
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Новая логика распределения значений по ячейкам ===
|
// Записываем все значения в соответствующие ячейки
|
||||||
if (!cells || cells.length === 0) return
|
cellValues.forEach(({ target, value: cellValue }) => {
|
||||||
|
const { row, col } = cellAddressToCoordinates(target.cell)
|
||||||
// 1) Массив (например, standards) -> каждой ячейке своё значение
|
if (target.sheet === 'Calculations') {
|
||||||
if (el.type === 'standards' && Array.isArray(value)) {
|
|
||||||
cells.forEach((tc, idx) => {
|
|
||||||
const cellValue = getStandardById(value[idx] as string)?.name || ''
|
|
||||||
const { row, col } = cellAddressToCoordinates(tc.cell)
|
|
||||||
if (tc.sheet === 'Calculations') {
|
|
||||||
toast.warning('Обнаружен лист Calculations')
|
toast.warning('Обнаружен лист Calculations')
|
||||||
}
|
}
|
||||||
const sheetName =
|
const sheetName =
|
||||||
tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
|
target.sheet === 'R' || target.sheet === 'Calculations'
|
||||||
|
? 'R'
|
||||||
|
: 'L'
|
||||||
engineRef.current.setCellValueWithoutRecalc(
|
engineRef.current.setCellValueWithoutRecalc(
|
||||||
sheetName,
|
sheetName,
|
||||||
row,
|
row,
|
||||||
@@ -532,85 +166,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
|||||||
cellValue
|
cellValue
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработка любых других массивов
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
cells.forEach((tc, idx) => {
|
|
||||||
const cellValue = value[idx] ?? ''
|
|
||||||
const { row, col } = cellAddressToCoordinates(tc.cell)
|
|
||||||
const sheetName =
|
|
||||||
tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
|
|
||||||
engineRef.current.setCellValueWithoutRecalc(
|
|
||||||
sheetName,
|
|
||||||
row,
|
|
||||||
col,
|
|
||||||
cellValue
|
|
||||||
)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Объект условия калибровки -> пишем свойства по порядку
|
|
||||||
if (
|
|
||||||
el.type === 'calibration_conditions' &&
|
|
||||||
typeof value === 'object' &&
|
|
||||||
value !== null
|
|
||||||
) {
|
|
||||||
const propsOrder = [
|
|
||||||
'temperature',
|
|
||||||
'humidity',
|
|
||||||
'pressure',
|
|
||||||
'voltage',
|
|
||||||
'frequency',
|
|
||||||
'lastUpdated',
|
|
||||||
] as const
|
|
||||||
cells.forEach((tc, idx) => {
|
|
||||||
const cellValue = (value as any)[propsOrder[idx]] ?? ''
|
|
||||||
const { row, col } = cellAddressToCoordinates(tc.cell)
|
|
||||||
const sheetName =
|
|
||||||
tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
|
|
||||||
engineRef.current.setCellValueWithoutRecalc(
|
|
||||||
sheetName,
|
|
||||||
row,
|
|
||||||
col,
|
|
||||||
cellValue
|
|
||||||
)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) Группа кнопок -> записываем label выбранной кнопки
|
|
||||||
if (el.type === 'button_group' && typeof value === 'string') {
|
|
||||||
const buttonOption = el.options?.find(opt => opt.value === value)
|
|
||||||
const cellValue = buttonOption ? buttonOption.label : value
|
|
||||||
cells.forEach(tc => {
|
|
||||||
const { row, col } = cellAddressToCoordinates(tc.cell)
|
|
||||||
const sheetName =
|
|
||||||
tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
|
|
||||||
engineRef.current.setCellValueWithoutRecalc(
|
|
||||||
sheetName,
|
|
||||||
row,
|
|
||||||
col,
|
|
||||||
cellValue
|
|
||||||
)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) Примитив или прочие объекты -> одно значение во все ячейки
|
|
||||||
cells.forEach(tc => {
|
|
||||||
const { row, col } = cellAddressToCoordinates(tc.cell)
|
|
||||||
const sheetName =
|
|
||||||
tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
|
|
||||||
engineRef.current.setCellValueWithoutRecalc(
|
|
||||||
sheetName,
|
|
||||||
row,
|
|
||||||
col,
|
|
||||||
value
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 2. Пересчитываем формулы
|
// 2. Пересчитываем формулы
|
||||||
@@ -630,8 +186,6 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// console.log('[DBG] Final cells_to_update', cellsToUpdate)
|
|
||||||
|
|
||||||
// Получаем последний файл для шаблона
|
// Получаем последний файл для шаблона
|
||||||
const latestFile = await getLatestFileForTemplate(template.id)
|
const latestFile = await getLatestFileForTemplate(template.id)
|
||||||
if (!latestFile) {
|
if (!latestFile) {
|
||||||
@@ -645,8 +199,6 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
|||||||
template_id: template.id,
|
template_id: template.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[DBG] POST body', body)
|
|
||||||
|
|
||||||
const resp = await fetch('/api/protocols/', {
|
const resp = await fetch('/api/protocols/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -746,13 +298,14 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
|||||||
|
|
||||||
{/* Основное содержимое */}
|
{/* Основное содержимое */}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
{/* Всегда монтируем DualSpreadsheet, скрываем/показываем через hidden */}
|
|
||||||
<div className={`${showSpreadsheet ? 'block' : 'hidden'} h-full`}>
|
<div className={`${showSpreadsheet ? 'block' : 'hidden'} h-full`}>
|
||||||
<DualSpreadsheet
|
<DualSpreadsheet
|
||||||
templateData={template.excelData ? { L: template.excelData } : {}}
|
templateData={template.excelData ? { L: template.excelData } : {}}
|
||||||
mergedCells={template.mergedCells || []}
|
mergedCells={template.mergedCells || []}
|
||||||
templateId={template.id}
|
templateId={template.id}
|
||||||
onEngineReady={engine => (engineRef.current = engine)}
|
onEngineReady={engine => {
|
||||||
|
engineRef.current = engine
|
||||||
|
}}
|
||||||
enableAutoSave={false}
|
enableAutoSave={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,8 +41,6 @@ export async function createSheet(
|
|||||||
sheet: CreateSheetInput
|
sheet: CreateSheetInput
|
||||||
): Promise<CreateSheetOutput> {
|
): Promise<CreateSheetOutput> {
|
||||||
try {
|
try {
|
||||||
// console.log('📤 Отправляем данные для создания листа:', sheet)
|
|
||||||
|
|
||||||
const response = await fetch('/api/sheets/', {
|
const response = await fetch('/api/sheets/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -65,7 +63,6 @@ export async function createSheet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result: CreateSheetOutput = await response.json()
|
const result: CreateSheetOutput = await response.json()
|
||||||
// console.log('✅ Лист создан успешно:', result)
|
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при создании листа:', error)
|
console.error('Ошибка при создании листа:', error)
|
||||||
@@ -100,8 +97,6 @@ export async function getSheetsByTemplate(
|
|||||||
templateId: string
|
templateId: string
|
||||||
): Promise<GetSheetsOutput> {
|
): Promise<GetSheetsOutput> {
|
||||||
try {
|
try {
|
||||||
// console.log('📋 Запрашиваем листы для templateId:', templateId)
|
|
||||||
|
|
||||||
// Получаем все листы и фильтруем на клиенте
|
// Получаем все листы и фильтруем на клиенте
|
||||||
const response = await fetch(`/api/sheets/`, {
|
const response = await fetch(`/api/sheets/`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -110,8 +105,6 @@ export async function getSheetsByTemplate(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// console.log('📋 Ответ сервера на запрос листов:', response.status)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// Пытаемся получить подробности ошибки от сервера
|
// Пытаемся получить подробности ошибки от сервера
|
||||||
let errorDetails = `HTTP error! status: ${response.status}`
|
let errorDetails = `HTTP error! status: ${response.status}`
|
||||||
@@ -183,7 +176,9 @@ export async function patchSheet(
|
|||||||
sheet: PatchSheetInput
|
sheet: PatchSheetInput
|
||||||
): Promise<PatchSheetOutput> {
|
): Promise<PatchSheetOutput> {
|
||||||
try {
|
try {
|
||||||
console.log('POST BODY:' + JSON.stringify(sheet))
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
console.log('PATCH BODY:', sheet)
|
||||||
|
}
|
||||||
const response = await fetch(`/api/sheets/${sheetId}`, {
|
const response = await fetch(`/api/sheets/${sheetId}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
|
host: '0.0.0.0', // Открыть для всех IP адресов
|
||||||
|
port: 80, // Явно указать порт (по умолчанию 5173)
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://192.169.0.163:8000',
|
target: 'http://192.169.0.163:8000',
|
||||||
|
|||||||
Reference in New Issue
Block a user