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 } // Валидационные правила 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(conditions) const [validationErrors, setValidationErrors] = useState< Record >({}) const [isLoading, setIsLoading] = useState(false) useEffect(() => { setTempConditions(conditions) }, [conditions]) const validateForm = () => { const errors: Record = {} 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 ( <> {/* Компактное отображение */}
{fieldConfigs.map(({ key, icon: Icon, unit }) => (
{conditions[key as keyof CalibrationConditions]} {unit}
))}
{isOutdated && (
)}
{/* Модальное окно */} {showModal && (
Условия поверки
{fieldConfigs.map( ({ key, label, icon: Icon, unit, placeholder }) => { const hasError = validationErrors[key] const isValid = tempConditions[key as keyof CalibrationConditions] && !hasError return (
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" />
{unit} {isValid && ( )} {hasError && (
{hasError}
)}
) } )}
Обновлено: {formatLastUpdated(conditions.lastUpdated)}
{isOutdated && ( Устарели )}
)} ) } interface CalibrationConditionsConfig { targetCells: any[] defaultConditions?: CalibrationConditions cellMapping?: { [key: string]: string // параметр -> ячейка из targetCells (по индексу) } } export const calibrationConditionsDefinition: ElementDefinition< CalibrationConditionsConfig, CalibrationConditions > = { type: 'calibration_conditions', label: 'Условия калибровки', icon: , 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 (

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

{/* Сопоставление параметров с ячейками */} {targetCells.length > 0 && (
{availableParams.map(param => (
{param.label}
))}

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

)}
onChange({ ...config, defaultConditions: { ...config.defaultConditions, temperature: e.target.value, humidity: config.defaultConditions?.humidity || '65', pressure: config.defaultConditions?.pressure || '101.3', voltage: config.defaultConditions?.voltage || '220', frequency: config.defaultConditions?.frequency || '50', lastUpdated: config.defaultConditions?.lastUpdated || new Date().toISOString(), }, }) } placeholder="20" />
onChange({ ...config, defaultConditions: { ...config.defaultConditions, temperature: config.defaultConditions?.temperature || '20', humidity: e.target.value, pressure: config.defaultConditions?.pressure || '101.3', voltage: config.defaultConditions?.voltage || '220', frequency: config.defaultConditions?.frequency || '50', lastUpdated: config.defaultConditions?.lastUpdated || new Date().toISOString(), }, }) } placeholder="65" />
onChange({ ...config, defaultConditions: { ...config.defaultConditions, temperature: config.defaultConditions?.temperature || '20', humidity: config.defaultConditions?.humidity || '65', pressure: e.target.value, voltage: config.defaultConditions?.voltage || '220', frequency: config.defaultConditions?.frequency || '50', lastUpdated: config.defaultConditions?.lastUpdated || new Date().toISOString(), }, }) } placeholder="101.3" />
onChange({ ...config, defaultConditions: { ...config.defaultConditions, temperature: config.defaultConditions?.temperature || '20', humidity: config.defaultConditions?.humidity || '65', pressure: config.defaultConditions?.pressure || '101.3', voltage: e.target.value, frequency: config.defaultConditions?.frequency || '50', lastUpdated: config.defaultConditions?.lastUpdated || new Date().toISOString(), }, }) } placeholder="220" />
onChange({ ...config, defaultConditions: { ...config.defaultConditions, temperature: config.defaultConditions?.temperature || '20', humidity: config.defaultConditions?.humidity || '65', pressure: config.defaultConditions?.pressure || '101.3', voltage: config.defaultConditions?.voltage || '220', frequency: e.target.value, lastUpdated: config.defaultConditions?.lastUpdated || new Date().toISOString(), }, }) } placeholder="50" />
) }, Preview: ({ config }) => (
Предпросмотр
true} />
), 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 ( { onChange?.(newConditions) return true }} /> ) }, }