781 lines
27 KiB
TypeScript
781 lines
27 KiB
TypeScript
import { Badge } from '@/component/ui/badge'
|
||
import { Button } from '@/component/ui/button'
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card'
|
||
import { Input } from '@/component/ui/input'
|
||
import { Label } from '@/component/ui/label'
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from '@/component/ui/select'
|
||
import { ElementDefinition } from '@/entitiy/element/model/interface'
|
||
import {
|
||
AlertCircle,
|
||
Check,
|
||
Droplets,
|
||
Edit3,
|
||
Gauge,
|
||
Radio,
|
||
Thermometer,
|
||
X,
|
||
Zap,
|
||
} from 'lucide-react'
|
||
import React, { useEffect, useState } from 'react'
|
||
|
||
interface CalibrationConditions {
|
||
temperature: string
|
||
humidity: string
|
||
pressure: string
|
||
voltage: string
|
||
frequency: string
|
||
lastUpdated: string
|
||
updatedBy?: string
|
||
}
|
||
|
||
interface ValidationResult {
|
||
isValid: boolean
|
||
message?: string
|
||
}
|
||
|
||
interface CalibrationCondInfoProps {
|
||
conditions: CalibrationConditions
|
||
isOutdated: boolean
|
||
onUpdate: (conditions: CalibrationConditions) => Promise<boolean>
|
||
}
|
||
|
||
// Валидационные правила
|
||
const validateField = (field: string, value: string): ValidationResult => {
|
||
const numValue = parseFloat(value)
|
||
|
||
if (!value.trim()) {
|
||
return { isValid: false, message: 'Поле обязательно' }
|
||
}
|
||
|
||
if (isNaN(numValue)) {
|
||
return { isValid: false, message: 'Введите число' }
|
||
}
|
||
|
||
switch (field) {
|
||
case 'temperature':
|
||
if (numValue < -40 || numValue > 85) {
|
||
return { isValid: false, message: 'Диапазон: -40...+85°C' }
|
||
}
|
||
break
|
||
case 'humidity':
|
||
if (numValue < 0 || numValue > 100) {
|
||
return { isValid: false, message: 'Диапазон: 0...100%' }
|
||
}
|
||
break
|
||
case 'pressure':
|
||
if (numValue < 80 || numValue > 120) {
|
||
return { isValid: false, message: 'Диапазон: 80...120 кПа' }
|
||
}
|
||
break
|
||
case 'voltage':
|
||
if (numValue < 180 || numValue > 250) {
|
||
return { isValid: false, message: 'Диапазон: 180...250 В' }
|
||
}
|
||
break
|
||
case 'frequency':
|
||
if (numValue < 45 || numValue > 65) {
|
||
return { isValid: false, message: 'Диапазон: 45...65 Гц' }
|
||
}
|
||
break
|
||
}
|
||
|
||
return { isValid: true }
|
||
}
|
||
|
||
const formatLastUpdated = (dateString: string) => {
|
||
const date = new Date(dateString)
|
||
const today = new Date()
|
||
const yesterday = new Date(today)
|
||
yesterday.setDate(yesterday.getDate() - 1)
|
||
|
||
const weekdayRu = date.toLocaleString('ru-RU', { weekday: 'long' })
|
||
const monthShortRu = date
|
||
.toLocaleString('ru-RU', { month: 'short' })
|
||
.replace(/\./, '')
|
||
.replace(/^./, s => s.toUpperCase())
|
||
const day = date.getDate()
|
||
const time: string = date.toLocaleString('ru-RU', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
})
|
||
|
||
if (date.toDateString() === today.toDateString()) {
|
||
return `Сегодня в ${time}`
|
||
} else if (date.toDateString() === yesterday.toDateString()) {
|
||
return `Вчера в ${time}`
|
||
} else {
|
||
return `${weekdayRu.replace(/^./, s =>
|
||
s.toUpperCase()
|
||
)} ${day} ${monthShortRu} ${time}`
|
||
}
|
||
}
|
||
|
||
function CalibrationCondInfo({
|
||
conditions,
|
||
isOutdated,
|
||
onUpdate,
|
||
}: CalibrationCondInfoProps) {
|
||
const [showModal, setShowModal] = useState(false)
|
||
const [tempConditions, setTempConditions] =
|
||
useState<CalibrationConditions>(conditions)
|
||
const [validationErrors, setValidationErrors] = useState<
|
||
Record<string, string>
|
||
>({})
|
||
const [isLoading, setIsLoading] = useState(false)
|
||
|
||
useEffect(() => {
|
||
setTempConditions(conditions)
|
||
}, [conditions])
|
||
|
||
const validateForm = () => {
|
||
const errors: Record<string, string> = {}
|
||
const fields = [
|
||
'temperature',
|
||
'humidity',
|
||
'pressure',
|
||
'voltage',
|
||
'frequency',
|
||
]
|
||
|
||
fields.forEach(field => {
|
||
const validation = validateField(
|
||
field,
|
||
tempConditions[field as keyof CalibrationConditions] as string
|
||
)
|
||
if (!validation.isValid && validation.message) {
|
||
errors[field] = validation.message
|
||
}
|
||
})
|
||
|
||
setValidationErrors(errors)
|
||
return Object.keys(errors).length === 0
|
||
}
|
||
|
||
const handleFieldChange = (field: string, value: string) => {
|
||
setTempConditions(prev => ({ ...prev, [field]: value }))
|
||
|
||
// Валидация в реальном времени
|
||
const validation = validateField(field, value)
|
||
setValidationErrors(prev => ({
|
||
...prev,
|
||
[field]: validation.isValid ? '' : validation.message || '',
|
||
}))
|
||
}
|
||
|
||
const handleSave = async () => {
|
||
if (!validateForm()) return
|
||
|
||
setIsLoading(true)
|
||
const newConditions = {
|
||
...tempConditions,
|
||
lastUpdated: new Date().toISOString(),
|
||
}
|
||
|
||
const success = await onUpdate(newConditions)
|
||
setIsLoading(false)
|
||
|
||
if (success) {
|
||
setShowModal(false)
|
||
} else {
|
||
alert('Не удалось сохранить условия поверки')
|
||
}
|
||
}
|
||
|
||
const openModal = () => {
|
||
setTempConditions(conditions)
|
||
setValidationErrors({})
|
||
setShowModal(true)
|
||
// Устанавливаем фокус на первое поле через небольшую задержку
|
||
setTimeout(() => {
|
||
const firstInput = document.getElementById(
|
||
'modal-temperature'
|
||
) as HTMLInputElement
|
||
if (firstInput) {
|
||
firstInput.focus()
|
||
firstInput.select()
|
||
}
|
||
}, 100)
|
||
}
|
||
|
||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||
if (
|
||
e.key === 'Enter' &&
|
||
!Object.keys(validationErrors).some(key => validationErrors[key])
|
||
) {
|
||
handleSave()
|
||
}
|
||
if (e.key === 'Escape') {
|
||
setShowModal(false)
|
||
}
|
||
}
|
||
|
||
const fieldConfigs = [
|
||
{
|
||
key: 'temperature',
|
||
label: 'Температура',
|
||
icon: Thermometer,
|
||
unit: '°C',
|
||
placeholder: '20',
|
||
},
|
||
{
|
||
key: 'humidity',
|
||
label: 'Влажность',
|
||
icon: Droplets,
|
||
unit: '%',
|
||
placeholder: '65',
|
||
},
|
||
{
|
||
key: 'pressure',
|
||
label: 'Давление',
|
||
icon: Gauge,
|
||
unit: 'кПа',
|
||
placeholder: '101.3',
|
||
},
|
||
{
|
||
key: 'voltage',
|
||
label: 'Напряжение',
|
||
icon: Zap,
|
||
unit: 'В',
|
||
placeholder: '220',
|
||
},
|
||
{
|
||
key: 'frequency',
|
||
label: 'Частота',
|
||
icon: Radio,
|
||
unit: 'Гц',
|
||
placeholder: '50',
|
||
},
|
||
]
|
||
|
||
return (
|
||
<>
|
||
{/* Компактное отображение */}
|
||
<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">
|
||
{fieldConfigs.map(({ key, icon: Icon, unit }) => (
|
||
<div key={key} className="flex items-center gap-1">
|
||
<Icon className="h-3 w-3 text-muted-foreground" />
|
||
<span className="text-foreground">
|
||
{conditions[key as keyof CalibrationConditions]}
|
||
{unit}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={openModal}
|
||
className={`h-6 w-6 p-0 ${
|
||
isOutdated ? 'text-yellow-600 hover:text-yellow-700' : ''
|
||
}`}
|
||
>
|
||
<Edit3 className="h-3 w-3" />
|
||
</Button>
|
||
{isOutdated && (
|
||
<div className="h-2 w-2 animate-pulse rounded-full bg-yellow-500" />
|
||
)}
|
||
</div>
|
||
|
||
{/* Модальное окно */}
|
||
{showModal && (
|
||
<div
|
||
className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto bg-black/50 p-4"
|
||
onKeyDown={handleKeyDown}
|
||
>
|
||
<div className="flex min-h-full items-center justify-center p-4">
|
||
<Card className="mx-auto my-8 w-full max-w-2xl">
|
||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
||
<CardTitle className="text-lg">Условия поверки</CardTitle>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => setShowModal(false)}
|
||
>
|
||
<X className="h-4 w-4" />
|
||
</Button>
|
||
</CardHeader>
|
||
<CardContent className="space-y-6">
|
||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||
{fieldConfigs.map(
|
||
({ key, label, icon: Icon, unit, placeholder }) => {
|
||
const hasError = validationErrors[key]
|
||
const isValid =
|
||
tempConditions[key as keyof CalibrationConditions] &&
|
||
!hasError
|
||
|
||
return (
|
||
<div key={key} className="space-y-2">
|
||
<Label
|
||
htmlFor={`modal-${key}`}
|
||
className="flex items-center gap-2"
|
||
>
|
||
<Icon className="h-4 w-4 text-muted-foreground" />
|
||
{label}
|
||
</Label>
|
||
<div className="space-y-1">
|
||
<div className="flex items-center gap-2">
|
||
<Input
|
||
id={`modal-${key}`}
|
||
value={
|
||
tempConditions[
|
||
key as keyof CalibrationConditions
|
||
] as string
|
||
}
|
||
onChange={e =>
|
||
handleFieldChange(key, e.target.value)
|
||
}
|
||
onKeyDown={e => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault()
|
||
handleSave()
|
||
}
|
||
}}
|
||
className={`text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none ${
|
||
hasError
|
||
? 'border-destructive focus-visible:ring-destructive'
|
||
: isValid
|
||
? 'border-green-500 focus-visible:ring-green-500'
|
||
: ''
|
||
}`}
|
||
placeholder={placeholder}
|
||
type="number"
|
||
step="0.1"
|
||
/>
|
||
<div className="flex min-w-[3rem] items-center gap-1">
|
||
<span className="text-sm text-muted-foreground">
|
||
{unit}
|
||
</span>
|
||
{isValid && (
|
||
<Check className="h-4 w-4 text-green-500" />
|
||
)}
|
||
{hasError && (
|
||
<div className="group relative">
|
||
<AlertCircle className="h-4 w-4 cursor-help text-destructive" />
|
||
<div className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2 transform whitespace-nowrap rounded border bg-popover px-2 py-1 text-xs text-popover-foreground opacity-0 shadow-md transition-opacity group-hover:opacity-100">
|
||
{hasError}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between border-t pt-4 text-xs">
|
||
<div className="flex items-center gap-4">
|
||
<span className="text-muted-foreground">
|
||
Обновлено: {formatLastUpdated(conditions.lastUpdated)}
|
||
</span>
|
||
</div>
|
||
{isOutdated && (
|
||
<Badge variant="destructive" className="text-xs">
|
||
Устарели
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex gap-3 pt-2">
|
||
<Button
|
||
onClick={handleSave}
|
||
className="flex-1"
|
||
disabled={
|
||
isLoading ||
|
||
Object.keys(validationErrors).some(
|
||
key => validationErrors[key]
|
||
)
|
||
}
|
||
>
|
||
{isLoading ? 'Сохранение...' : 'Сохранить'}
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => setShowModal(false)}
|
||
className="flex-1"
|
||
disabled={isLoading}
|
||
>
|
||
Отмена
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)
|
||
}
|
||
|
||
interface CalibrationConditionsConfig {
|
||
targetCells: any[]
|
||
defaultConditions?: CalibrationConditions
|
||
cellMapping?: {
|
||
[key: string]: string // параметр -> ячейка из targetCells (по индексу)
|
||
}
|
||
}
|
||
|
||
export const calibrationConditionsDefinition: ElementDefinition<
|
||
CalibrationConditionsConfig,
|
||
CalibrationConditions
|
||
> = {
|
||
type: 'calibration_conditions',
|
||
label: 'Условия калибровки',
|
||
icon: <Thermometer className="h-4 w-4" />,
|
||
version: 1,
|
||
defaultConfig: {
|
||
targetCells: [],
|
||
cellMapping: {},
|
||
defaultConditions: {
|
||
temperature: '20',
|
||
humidity: '65',
|
||
pressure: '101.3',
|
||
voltage: '220',
|
||
frequency: '50',
|
||
lastUpdated: new Date().toISOString(),
|
||
},
|
||
},
|
||
mapToCells: cfg => {
|
||
// Используем только ячейки, указанные пользователем в targetCells
|
||
return cfg.targetCells || []
|
||
},
|
||
mapToCellValues: (config, value) => {
|
||
// Используем targetCells из конфигурации
|
||
const cells = config.targetCells || []
|
||
const cellMapping = config.cellMapping || {}
|
||
|
||
// Если значение не объект, возвращаем пустые значения
|
||
if (!value || typeof value !== 'object') {
|
||
return cells.map(target => ({
|
||
target,
|
||
value: '',
|
||
}))
|
||
}
|
||
|
||
// Создаем обратное сопоставление: индекс ячейки -> параметр
|
||
const reversedMapping: { [cellIndex: string]: string } = {}
|
||
Object.entries(cellMapping).forEach(([param, cellIndex]) => {
|
||
reversedMapping[cellIndex] = param
|
||
})
|
||
|
||
return cells.map((target, idx) => {
|
||
const paramKey = reversedMapping[idx.toString()]
|
||
const paramValue = paramKey ? (value as any)[paramKey] : ''
|
||
|
||
return {
|
||
target,
|
||
value: paramValue || '',
|
||
}
|
||
})
|
||
},
|
||
Editor: ({ config, onChange }) => {
|
||
const availableParams = [
|
||
{ key: 'temperature', label: 'Температура' },
|
||
{ key: 'humidity', label: 'Влажность' },
|
||
{ key: 'pressure', label: 'Давление' },
|
||
{ key: 'voltage', label: 'Напряжение' },
|
||
{ key: 'frequency', label: 'Частота' },
|
||
{ key: 'lastUpdated', label: 'Дата обновления' },
|
||
]
|
||
|
||
const targetCells = config.targetCells || []
|
||
const cellMapping = config.cellMapping || {}
|
||
|
||
// Функция для создания автоматического сопоставления по порядку
|
||
const createAutoMapping = () => {
|
||
const newMapping: { [key: string]: string } = {}
|
||
const paramKeys = availableParams.map(p => p.key)
|
||
|
||
// Сопоставляем первые N параметров с первыми N ячейками
|
||
targetCells.forEach((_, index) => {
|
||
if (index < paramKeys.length) {
|
||
newMapping[paramKeys[index]] = index.toString()
|
||
}
|
||
})
|
||
|
||
onChange({
|
||
...config,
|
||
cellMapping: newMapping,
|
||
})
|
||
}
|
||
|
||
const updateCellMapping = (param: string, cellIndex: string) => {
|
||
const newMapping = { ...cellMapping }
|
||
if (cellIndex === '' || cellIndex === 'none') {
|
||
delete newMapping[param]
|
||
} else {
|
||
newMapping[param] = cellIndex
|
||
}
|
||
onChange({
|
||
...config,
|
||
cellMapping: newMapping,
|
||
})
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||
<p className="text-sm text-gray-600">
|
||
Элемент "Условия калибровки" позволяет вводить и отслеживать
|
||
параметры окружающей среды при проведении измерений. Поддерживает
|
||
валидацию значений и отслеживание времени обновления.
|
||
</p>
|
||
</div>
|
||
|
||
{/* Сопоставление параметров с ячейками */}
|
||
{targetCells.length > 0 && (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<Label className="text-sm font-medium">
|
||
Сопоставление с ячейками
|
||
</Label>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={createAutoMapping}
|
||
className="h-7 px-2 text-xs"
|
||
>
|
||
Автозаполнение
|
||
</Button>
|
||
</div>
|
||
<div className="space-y-3 rounded-lg border border-gray-200 p-4">
|
||
{availableParams.map(param => (
|
||
<div key={param.key} className="flex items-center gap-3">
|
||
<div className="min-w-[120px]">
|
||
<span className="text-sm text-gray-700">{param.label}</span>
|
||
</div>
|
||
<div className="flex-1">
|
||
<Select
|
||
value={cellMapping[param.key] || 'none'}
|
||
onValueChange={value =>
|
||
updateCellMapping(param.key, value)
|
||
}
|
||
>
|
||
<SelectTrigger className="w-full">
|
||
<SelectValue placeholder="Не указано" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="none">Не указано</SelectItem>
|
||
{targetCells.map((cell, index) => (
|
||
<SelectItem key={index} value={index.toString()}>
|
||
{cell.sheet}!{cell.cell}{' '}
|
||
{cell.displayName ? `(${cell.displayName})` : ''}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<p className="text-xs text-gray-500">
|
||
Настройте какой параметр условий калибровки будет записан в какую
|
||
ячейку. Используйте кнопку "Автозаполнение" для автоматического
|
||
сопоставления по порядку.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-medium">Параметры по умолчанию</label>
|
||
<div className="grid grid-cols-2 gap-4 rounded-lg border border-gray-200 p-4">
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-medium text-gray-600">
|
||
Температура (°C)
|
||
</label>
|
||
<Input
|
||
type="number"
|
||
value={config.defaultConditions?.temperature || '20'}
|
||
onChange={e =>
|
||
onChange({
|
||
...config,
|
||
defaultConditions: {
|
||
...config.defaultConditions,
|
||
temperature: e.target.value,
|
||
humidity: config.defaultConditions?.humidity || '65',
|
||
pressure: config.defaultConditions?.pressure || '101.3',
|
||
voltage: config.defaultConditions?.voltage || '220',
|
||
frequency: config.defaultConditions?.frequency || '50',
|
||
lastUpdated:
|
||
config.defaultConditions?.lastUpdated ||
|
||
new Date().toISOString(),
|
||
},
|
||
})
|
||
}
|
||
placeholder="20"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-medium text-gray-600">
|
||
Влажность (%)
|
||
</label>
|
||
<Input
|
||
type="number"
|
||
value={config.defaultConditions?.humidity || '65'}
|
||
onChange={e =>
|
||
onChange({
|
||
...config,
|
||
defaultConditions: {
|
||
...config.defaultConditions,
|
||
temperature:
|
||
config.defaultConditions?.temperature || '20',
|
||
humidity: e.target.value,
|
||
pressure: config.defaultConditions?.pressure || '101.3',
|
||
voltage: config.defaultConditions?.voltage || '220',
|
||
frequency: config.defaultConditions?.frequency || '50',
|
||
lastUpdated:
|
||
config.defaultConditions?.lastUpdated ||
|
||
new Date().toISOString(),
|
||
},
|
||
})
|
||
}
|
||
placeholder="65"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-medium text-gray-600">
|
||
Давление (кПа)
|
||
</label>
|
||
<Input
|
||
type="number"
|
||
value={config.defaultConditions?.pressure || '101.3'}
|
||
onChange={e =>
|
||
onChange({
|
||
...config,
|
||
defaultConditions: {
|
||
...config.defaultConditions,
|
||
temperature:
|
||
config.defaultConditions?.temperature || '20',
|
||
humidity: config.defaultConditions?.humidity || '65',
|
||
pressure: e.target.value,
|
||
voltage: config.defaultConditions?.voltage || '220',
|
||
frequency: config.defaultConditions?.frequency || '50',
|
||
lastUpdated:
|
||
config.defaultConditions?.lastUpdated ||
|
||
new Date().toISOString(),
|
||
},
|
||
})
|
||
}
|
||
placeholder="101.3"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-medium text-gray-600">
|
||
Напряжение (В)
|
||
</label>
|
||
<Input
|
||
type="number"
|
||
value={config.defaultConditions?.voltage || '220'}
|
||
onChange={e =>
|
||
onChange({
|
||
...config,
|
||
defaultConditions: {
|
||
...config.defaultConditions,
|
||
temperature:
|
||
config.defaultConditions?.temperature || '20',
|
||
humidity: config.defaultConditions?.humidity || '65',
|
||
pressure: config.defaultConditions?.pressure || '101.3',
|
||
voltage: e.target.value,
|
||
frequency: config.defaultConditions?.frequency || '50',
|
||
lastUpdated:
|
||
config.defaultConditions?.lastUpdated ||
|
||
new Date().toISOString(),
|
||
},
|
||
})
|
||
}
|
||
placeholder="220"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-medium text-gray-600">
|
||
Частота (Гц)
|
||
</label>
|
||
<Input
|
||
type="number"
|
||
value={config.defaultConditions?.frequency || '50'}
|
||
onChange={e =>
|
||
onChange({
|
||
...config,
|
||
defaultConditions: {
|
||
...config.defaultConditions,
|
||
temperature:
|
||
config.defaultConditions?.temperature || '20',
|
||
humidity: config.defaultConditions?.humidity || '65',
|
||
pressure: config.defaultConditions?.pressure || '101.3',
|
||
voltage: config.defaultConditions?.voltage || '220',
|
||
frequency: e.target.value,
|
||
lastUpdated:
|
||
config.defaultConditions?.lastUpdated ||
|
||
new Date().toISOString(),
|
||
},
|
||
})
|
||
}
|
||
placeholder="50"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
},
|
||
Preview: ({ config }) => (
|
||
<div className="space-y-3">
|
||
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
|
||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
||
<CalibrationCondInfo
|
||
conditions={
|
||
config.defaultConditions || {
|
||
temperature: '20',
|
||
humidity: '65',
|
||
pressure: '101.3',
|
||
voltage: '220',
|
||
frequency: '50',
|
||
lastUpdated: new Date().toISOString(),
|
||
}
|
||
}
|
||
isOutdated={false}
|
||
onUpdate={async () => true}
|
||
/>
|
||
</div>
|
||
</div>
|
||
),
|
||
Render: ({ config, value, onChange }) => {
|
||
// Если значение не установлено, используем defaultConditions из конфигурации
|
||
const conditions = value ||
|
||
config.defaultConditions || {
|
||
temperature: '20',
|
||
humidity: '65',
|
||
pressure: '101.3',
|
||
voltage: '220',
|
||
frequency: '50',
|
||
lastUpdated: new Date().toISOString(),
|
||
}
|
||
|
||
// Проверяем, устарели ли условия (больше 24 часов)
|
||
const lastUpdated = new Date(conditions.lastUpdated)
|
||
const now = new Date()
|
||
const isOutdated =
|
||
now.getTime() - lastUpdated.getTime() > 24 * 60 * 60 * 1000
|
||
|
||
return (
|
||
<CalibrationCondInfo
|
||
conditions={conditions}
|
||
isOutdated={isOutdated}
|
||
onUpdate={async newConditions => {
|
||
onChange?.(newConditions)
|
||
return true
|
||
}}
|
||
/>
|
||
)
|
||
},
|
||
}
|