From 1f021364556ae5b0c8be8c2df787a8c90ad95aa7 Mon Sep 17 00:00:00 2001 From: tlartem Date: Thu, 24 Jul 2025 19:56:58 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BF=D0=BE=D1=87=D1=82=D0=B8=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=87=D0=B8=D0=B9=20=D0=B2=D0=B0=D1=80=D0=B8=D0=B0?= =?UTF-8?q?=D0=BD=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + .../CalibrationConditionsElement.tsx | 782 ------------------ src/component/BasicElements/index.ts | 1 - .../StandardsElement/StandardsDialog.tsx | 379 --------- .../StandardsElement/StandardsPreview.tsx | 72 -- src/component/StandardsElement/definition.tsx | 155 ---- .../StandardsElement/mockStandards.ts | 229 ----- .../TemplateManager/ElementConstructor.tsx | 95 +-- .../VerificationConditionsEditor.tsx | 341 ++++---- .../VerificationConditionsPreview.tsx | 267 +++--- .../VerificationConditionsRender.tsx | 555 +++++++------ .../VerificationConditionsElement/api.ts | 269 +++--- .../definition.tsx | 92 +-- .../VerificationConditionsElement/hooks.ts | 509 ++++-------- .../VerificationConditionsElement/index.ts | 2 +- .../VerificationConditionsElement/mockData.ts | 282 +------ .../VerificationConditionsElement/types.ts | 178 ++-- src/component/ui/calendar.tsx | 213 +++++ .../StandardsElement/StandardsEditor.tsx | 12 +- .../StandardsElement/StandardsPreview.tsx | 52 ++ .../StandardsElement/StandardsSelector.tsx | 0 .../implementations}/StandardsElement/api.ts | 0 .../StandardsElement/definition.tsx | 188 +++++ .../StandardsElement/hooks.ts | 0 .../StandardsElement/types.ts | 0 src/entitiy/element/model/interface.ts | 7 +- src/page/StandardsManagementPage.tsx | 22 +- tailwind.config.js | 104 +-- yarn.lock | 24 + 29 files changed, 1627 insertions(+), 3205 deletions(-) delete mode 100644 src/component/BasicElements/CalibrationConditionsElement.tsx delete mode 100644 src/component/StandardsElement/StandardsDialog.tsx delete mode 100644 src/component/StandardsElement/StandardsPreview.tsx delete mode 100644 src/component/StandardsElement/definition.tsx delete mode 100644 src/component/StandardsElement/mockStandards.ts create mode 100644 src/component/ui/calendar.tsx rename src/{component => entitiy/element/model/implementations}/StandardsElement/StandardsEditor.tsx (91%) create mode 100644 src/entitiy/element/model/implementations/StandardsElement/StandardsPreview.tsx rename src/{component => entitiy/element/model/implementations}/StandardsElement/StandardsSelector.tsx (100%) rename src/{component => entitiy/element/model/implementations}/StandardsElement/api.ts (100%) create mode 100644 src/entitiy/element/model/implementations/StandardsElement/definition.tsx rename src/{component => entitiy/element/model/implementations}/StandardsElement/hooks.ts (100%) rename src/{component => entitiy/element/model/implementations}/StandardsElement/types.ts (100%) diff --git a/package.json b/package.json index e96df00..d7a0895 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@types/react-window": "^1.8.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", "immer": "^10.1.1", @@ -41,6 +42,7 @@ "lucide-react": "^0.525.0", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", + "react-day-picker": "^9.8.0", "react-dom": "^18.2.0", "react-grid-layout": "^1.5.2", "react-rnd": "^10.5.2", diff --git a/src/component/BasicElements/CalibrationConditionsElement.tsx b/src/component/BasicElements/CalibrationConditionsElement.tsx deleted file mode 100644 index c7894a8..0000000 --- a/src/component/BasicElements/CalibrationConditionsElement.tsx +++ /dev/null @@ -1,782 +0,0 @@ -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 { cn } from '@/lib/utils' -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 - }} - /> - ) - }, -} diff --git a/src/component/BasicElements/index.ts b/src/component/BasicElements/index.ts index 55fcb07..d9e0bb6 100644 --- a/src/component/BasicElements/index.ts +++ b/src/component/BasicElements/index.ts @@ -1,6 +1,5 @@ export { textDefinition } from '@/entitiy/element/model/implementations/TextElement' export { buttonGroupDefinition } from '../../entitiy/element/model/implementations/ButtonGroup' export { dateDefinition } from '../../entitiy/element/model/implementations/DateElement' -export { calibrationConditionsDefinition } from './CalibrationConditionsElement' export { numberDefinition } from './NumberElement' export { selectDefinition } from './SelectElement' diff --git a/src/component/StandardsElement/StandardsDialog.tsx b/src/component/StandardsElement/StandardsDialog.tsx deleted file mode 100644 index 9353afa..0000000 --- a/src/component/StandardsElement/StandardsDialog.tsx +++ /dev/null @@ -1,379 +0,0 @@ -import { Button } from '@/component/ui/button' -import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card' -import { Input } from '@/component/ui/input' -import { Separator } from '@/component/ui/separator' -import { useToast } from '@/lib/hooks/useToast' -import { Check, FileText, Gauge, Search, Settings, X } from 'lucide-react' -import { useEffect, useMemo, useRef, useState } from 'react' - -interface MeasurementStandard { - id: string - name: string - shortName: string - type: string - registryNumber: string - range: string - accuracy: string - certificateNumber: string - validUntil: string -} - -interface StandardsConfig { - registryId: string - selectedStandards: string[] - lastUpdated: string -} - -interface StandardsDialogProps { - isOpen: boolean - onClose: () => void - standards: MeasurementStandard[] - config: StandardsConfig - onSave: (config: StandardsConfig) => void -} - -export function StandardsDialog({ - isOpen, - onClose, - standards, - config, - onSave, -}: StandardsDialogProps) { - const [selectedStandards, setSelectedStandards] = useState( - config.selectedStandards - ) - const [searchTerm, setSearchTerm] = useState('') - const { error } = useToast() - const searchInputRef = useRef(null) - - // Автофокус на поле поиска при открытии - useEffect(() => { - if (isOpen && searchInputRef.current) { - // Небольшая задержка для корректной работы с анимацией диалога - const timer = setTimeout(() => { - searchInputRef.current?.focus() - }, 150) - return () => clearTimeout(timer) - } - }, [isOpen]) - - // Обработка клавиш для автоматического ввода в поиск - useEffect(() => { - if (!isOpen) return - - const handleKeyDown = (event: KeyboardEvent) => { - // Игнорируем если фокус на интерактивных элементах - const activeElement = document.activeElement - if ( - activeElement?.tagName === 'INPUT' || - activeElement?.tagName === 'TEXTAREA' || - activeElement?.tagName === 'BUTTON' || - activeElement?.getAttribute('contenteditable') === 'true' - ) { - return - } - - // Игнорируем системные клавиши - if ( - event.ctrlKey || - event.metaKey || - event.altKey || - event.key === 'Tab' || - event.key === 'Enter' || - event.key === 'Escape' || - event.key === 'ArrowUp' || - event.key === 'ArrowDown' || - event.key === 'ArrowLeft' || - event.key === 'ArrowRight' - ) { - return - } - - // Обработка Backspace для удаления последнего символа - if (event.key === 'Backspace') { - event.preventDefault() - setSearchTerm(prev => prev.slice(0, -1)) - searchInputRef.current?.focus() - return - } - - // Печатные символы - if (event.key.length === 1) { - event.preventDefault() - setSearchTerm(prev => prev + event.key) - searchInputRef.current?.focus() - } - } - - document.addEventListener('keydown', handleKeyDown) - return () => document.removeEventListener('keydown', handleKeyDown) - }, [isOpen]) - - if (!isOpen) return null - - const filteredStandards = useMemo(() => { - if (!searchTerm) return standards - - const searchLower = searchTerm.toLowerCase() - return standards.filter( - standard => - standard.name.toLowerCase().includes(searchLower) || - standard.shortName.toLowerCase().includes(searchLower) || - standard.type.toLowerCase().includes(searchLower) || - standard.registryNumber.toLowerCase().includes(searchLower) - ) - }, [searchTerm, standards]) - - const selectedStandardsData = standards.filter(standard => - selectedStandards.includes(standard.id) - ) - - const unselectedStandards = filteredStandards.filter( - standard => !selectedStandards.includes(standard.id) - ) - - const isMaxReached = selectedStandards.length >= 7 - - const toggleStandard = (standardId: string) => { - setSelectedStandards(prev => { - if (prev.includes(standardId)) { - return prev.filter(id => id !== standardId) - } else if (prev.length < 7) { - return [...prev, standardId] - } else { - error('Можно выбрать максимум 7 эталонов', { - title: 'Достигнут лимит', - }) - return prev - } - }) - } - - const removeStandard = (standardId: string) => { - setSelectedStandards(prev => prev.filter(id => id !== standardId)) - } - - const handleSave = () => { - onSave({ - ...config, - selectedStandards, - lastUpdated: new Date().toISOString(), - }) - onClose() - } - - const handleClose = () => { - setSelectedStandards(config.selectedStandards) // Сбрасываем к исходному значению - setSearchTerm('') - onClose() - } - - const isExpiringSoon = (validUntil: string) => { - const expiryDate = new Date(validUntil) - const now = new Date() - const monthsUntilExpiry = - (expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24 * 30) - return monthsUntilExpiry <= 3 - } - - const renderStandardCard = ( - standard: MeasurementStandard, - isSelected: boolean, - showRemoveButton = false - ) => { - const isDisabled = !isSelected && isMaxReached && !showRemoveButton - - return ( -
toggleStandard(standard.id) - : undefined - } - > - {!showRemoveButton && ( -
- {isSelected && ( - - )} -
- )} - -
-
-
- {standard.shortName} -
-
- {standard.name} -
-
-
- -
-
- - {standard.registryNumber} -
-
- - {standard.range} -
-
- - {showRemoveButton && ( - - )} -
- ) - } - - return ( -
- - -
-
- - - Измерительные эталоны - - ({selectedStandards.length}/7) - - -
-
-
- - - {/* Поиск */} -
- - setSearchTerm(e.target.value)} - className="pl-10" - autoComplete="off" - autoFocus - /> - {searchTerm && ( - - )} -
- - {/* Контент с прокруткой */} -
- {/* Выбранные эталоны */} - {selectedStandardsData.length > 0 && ( -
-

- Выбранные эталоны ({selectedStandardsData.length}) -

-
- {selectedStandardsData.map(standard => - renderStandardCard(standard, true, true) - )} -
-
- )} - - {selectedStandardsData.length > 0 && - unselectedStandards.length > 0 && } - - {/* Доступные эталоны */} - {unselectedStandards.length > 0 && ( -
-

- Доступные эталоны ({unselectedStandards.length}) - {isMaxReached && ( - - Достигнут лимит выбора - - )} -

-
- {unselectedStandards.map(standard => - renderStandardCard(standard, false) - )} -
-
- )} - - {filteredStandards.length === 0 && searchTerm && ( -
- Эталоны не найдены по запросу "{searchTerm}" -
- )} -
- - - - {/* Зафиксированные кнопки */} -
-
- - -
-
-
-
-
- ) -} diff --git a/src/component/StandardsElement/StandardsPreview.tsx b/src/component/StandardsElement/StandardsPreview.tsx deleted file mode 100644 index 53a7460..0000000 --- a/src/component/StandardsElement/StandardsPreview.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Badge } from '@/component/ui/badge' -import { Button } from '@/component/ui/button' -import { Label } from '@/component/ui/label' -import { FileText, Settings } from 'lucide-react' -import { mockStandards } from './mockStandards' - -interface StandardsConfig { - sheet: string - startCell: string - maxItems: number - targetCells: any[] -} - -interface StandardsPreviewProps { - config: StandardsConfig -} - -export const StandardsPreview: React.FC = ({ - config, -}) => { - // Берем первые config.maxItems эталонов для предпросмотра - const previewStandards = mockStandards.slice(0, config.maxItems || 7) - - return ( -
- {/* Блок с измерительными эталонами */} -
-
- - -
- -
-
- {previewStandards.map((standard, index) => ( -
-
- - {index + 1} - -
-
-
- {standard.name} -
-
- - - {standard.registryNumber} - -
-
- - {standard.range} - -
- ))} -
-
-
-
- ) -} diff --git a/src/component/StandardsElement/definition.tsx b/src/component/StandardsElement/definition.tsx deleted file mode 100644 index 1d1be66..0000000 --- a/src/component/StandardsElement/definition.tsx +++ /dev/null @@ -1,155 +0,0 @@ -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 { CellTarget } from '@/type/template' -import { FileText, Settings, Weight } from 'lucide-react' -import { useEffect, useState } from 'react' -import { useStandards } from './hooks' -import { getStandardById } from './mockStandards' -import { StandardsEditor } from './StandardsEditor' -import { StandardsPreview } from './StandardsPreview' -import { StandardsSelector } from './StandardsSelector' - -interface StandardsConfig { - sheet: string // Лист Excel - startCell: string // Первая ячейка (например "A10") - maxItems: number // Обычно 7 - targetCells: CellTarget[] - selectedStandards?: string[] // Выбранные эталоны (ID) -} - -export const standardsDefinition: ElementDefinition = - { - type: 'standards', - label: 'Измерительные эталоны', - icon: , - version: 1, - defaultConfig: { - sheet: 'L', - startCell: 'A1', - maxItems: 7, - targetCells: [], - selectedStandards: [], // По умолчанию пустой массив - }, - Editor: StandardsEditor, - Preview: StandardsPreview, // Использует моки для демонстрации - Render: ({ config, value, onChange }) => { - const [isStandardsDialogOpen, setIsStandardsDialogOpen] = useState(false) - const [selectedStandardIds, setSelectedStandardIds] = useState( - // Инициализируем из config.selectedStandards или value - config.selectedStandards || (Array.isArray(value) ? value : []) - ) - - // Получаем реальные данные эталонов - const { data: standards = [] } = useStandards() - - // Синхронизируем selectedStandardIds с config и value - useEffect(() => { - const standards = - config.selectedStandards || (Array.isArray(value) ? value : []) - setSelectedStandardIds(standards) - }, [config.selectedStandards, value]) - - // Получаем данные выбранных эталонов из реального API - const selectedStandardsData = selectedStandardIds - .map(id => standards.find(s => s.id === id)) - .filter(Boolean) - - return ( -
- {/* Блок с измерительными эталонами */} -
-
- - -
- -
- {selectedStandardsData.length > 0 ? ( -
- {selectedStandardsData.map( - (standard, index) => - standard && ( -
-
- - {index + 1} - -
-
-
- {standard.name} -
-
- - - {standard.registry_number} - -
-
- - {standard.range} - -
- ) - )} -
- ) : ( -
- - Эталоны не выбраны -
- )} -
-
- - setIsStandardsDialogOpen(false)} - value={selectedStandardIds} - onChange={standardIds => { - setSelectedStandardIds(standardIds) - // Сохраняем ID эталонов в value для записи в Excel - onChange?.(standardIds) - }} - maxItems={config.maxItems} - /> -
- ) - }, - mapToCells: cfg => { - // Используем только ячейки, указанные пользователем в targetCells - return cfg.targetCells || [] - }, - mapToCellValues: (cfg, value) => { - // Используем targetCells из конфигурации - const cells = cfg.targetCells || [] - - // Конвертируем ID эталонов в их названия для записи в Excel - // Здесь используем моки для быстрого доступа к данным при записи в Excel - const standardIds = Array.isArray(value) ? value : [] - const standardNames = standardIds.map( - id => getStandardById(id)?.name || '' - ) - - return cells.map((target, idx) => ({ - target, - value: standardNames[idx] || '', // Берем значение по индексу или пустую строку - })) - }, - } diff --git a/src/component/StandardsElement/mockStandards.ts b/src/component/StandardsElement/mockStandards.ts deleted file mode 100644 index 6bcd476..0000000 --- a/src/component/StandardsElement/mockStandards.ts +++ /dev/null @@ -1,229 +0,0 @@ -export interface MeasurementStandard { - id: string - name: string - shortName: string - type: string - registryNumber: string - range: string - accuracy: string - certificateNumber: string - validUntil: string -} - -// Общие моковые данные для эталонов -export const mockStandards: MeasurementStandard[] = [ - { - 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: '6', - name: 'Эталон электрического напряжения 10 В', - shortName: 'ЭН-10В', - type: 'Электричество', - registryNumber: 'ГРСИ 12345-06', - range: '1-100 В', - accuracy: '±0.01 В', - certificateNumber: 'СИ-2024-006', - validUntil: '2025-08-15', - }, - { - id: '7', - name: 'Эталон силы тока 1 А', - shortName: 'ЭТ-1А', - type: 'Электричество', - registryNumber: 'ГРСИ 12345-07', - range: '0.1-10 А', - accuracy: '±0.001 А', - certificateNumber: 'СИ-2024-007', - validUntil: '2025-11-30', - }, - { - id: '8', - name: 'Эталон частоты 1 кГц', - shortName: 'ЭЧ-1кГц', - type: 'Частота', - registryNumber: 'ГРСИ 12345-08', - range: '10 Гц - 10 кГц', - accuracy: '±0.0001 Гц', - certificateNumber: 'СИ-2024-008', - validUntil: '2025-07-20', - }, - { - id: '9', - name: 'Эталон влажности 50% RH', - shortName: 'ЭВ-50%', - type: 'Влажность', - registryNumber: 'ГРСИ 12345-09', - range: '10-90% RH', - accuracy: '±0.5% RH', - certificateNumber: 'СИ-2024-009', - validUntil: '2025-04-10', - }, - { - id: '10', - name: 'Эталон освещенности 1000 лк', - shortName: 'ЭО-1000лк', - type: 'Освещенность', - registryNumber: 'ГРСИ 12345-10', - range: '1-10000 лк', - accuracy: '±1 лк', - certificateNumber: 'СИ-2024-010', - validUntil: '2025-10-05', - }, - { - id: '11', - name: 'Эталон скорости 10 м/с', - shortName: 'ЭС-10м/с', - type: 'Скорость', - registryNumber: 'ГРСИ 12345-11', - range: '0.1-50 м/с', - accuracy: '±0.01 м/с', - certificateNumber: 'СИ-2024-011', - validUntil: '2025-05-25', - }, - { - id: '12', - name: 'Эталон ускорения 9.8 м/с²', - shortName: 'ЭУ-9.8м/с²', - type: 'Ускорение', - registryNumber: 'ГРСИ 12345-12', - range: '0.1-20 м/с²', - accuracy: '±0.001 м/с²', - certificateNumber: 'СИ-2024-012', - validUntil: '2025-12-15', - }, - { - id: '13', - name: 'Эталон магнитной индукции 1 Тл', - shortName: 'ЭМИ-1Тл', - type: 'Магнетизм', - registryNumber: 'ГРСИ 12345-13', - range: '0.01-10 Тл', - accuracy: '±0.0001 Тл', - certificateNumber: 'СИ-2024-013', - validUntil: '2025-09-08', - }, - { - id: '14', - name: 'Эталон акустического давления 1 Па', - shortName: 'ЭАД-1Па', - type: 'Акустика', - registryNumber: 'ГРСИ 12345-14', - range: '0.02-200 Па', - accuracy: '±0.1 дБ', - certificateNumber: 'СИ-2024-014', - validUntil: '2025-06-12', - }, - { - id: '15', - name: 'Эталон угла поворота 1°', - shortName: 'ЭУП-1°', - type: 'Угол', - registryNumber: 'ГРСИ 12345-15', - range: '0-360°', - accuracy: '±0.001°', - certificateNumber: 'СИ-2024-015', - validUntil: '2025-08-30', - }, -] - -// Утилиты для работы с эталонами -export const getStandardById = ( - id: string -): MeasurementStandard | undefined => { - return mockStandards.find(standard => standard.id === id) -} - -export const getStandardByName = ( - name: string -): MeasurementStandard | undefined => { - return mockStandards.find(standard => standard.name === name) -} - -// Функция для получения полезного badge по типу эталона -export const getStandardTypeBadge = (type: string) => { - switch (type) { - case 'Масса': - return 'МАССА' - case 'Длина': - return 'ДЛИНА' - case 'Температура': - return 'ТЕМП' - case 'Давление': - return 'ДАВЛ' - case 'Время': - return 'ВРЕМЯ' - case 'Электричество': - return 'ЭЛЕКТ' - case 'Частота': - return 'ЧАСТ' - case 'Влажность': - return 'ВЛАЖ' - case 'Освещенность': - return 'СВЕТ' - case 'Скорость': - return 'СКОР' - case 'Ускорение': - return 'УСКОР' - case 'Магнетизм': - return 'МАГН' - case 'Акустика': - return 'ЗВУК' - case 'Угол': - return 'УГОЛ' - default: - return 'ЭТЛ' - } -} diff --git a/src/component/TemplateManager/ElementConstructor.tsx b/src/component/TemplateManager/ElementConstructor.tsx index a419e97..a6471b6 100644 --- a/src/component/TemplateManager/ElementConstructor.tsx +++ b/src/component/TemplateManager/ElementConstructor.tsx @@ -139,7 +139,7 @@ const CellTargetEditor: React.FC<{ {/* Поле ввода ячейки */} handleCellInputChange(index, e.target.value)} className="h-6 w-16 px-2 text-xs" @@ -231,36 +231,6 @@ const ElementForm: React.FC = ({ formData, setFormData }) => { ? getElementDefinition(formData.type) : null - const handleAddOption = () => { - setFormData(prev => ({ - ...prev, - options: [...(prev.options || []), { value: '', label: '' }], - })) - } - - const handleUpdateOption = ( - index: number, - field: 'value' | 'label', - value: string - ) => { - setFormData(prev => ({ - ...prev, - options: prev.options?.map((option, i) => - i === index ? { ...option, [field]: value } : option - ), - })) - } - - const handleRemoveOption = (index: number) => { - setFormData(prev => ({ - ...prev, - options: prev.options?.filter((_, i) => i !== index), - })) - } - - const needsOptions = - formData.type === 'select' || formData.type === 'button_group' - return (
{/* Настройки элемента */} @@ -394,69 +364,6 @@ const ElementForm: React.FC = ({ formData, setFormData }) => { )} - - {/* Варианты для select/radio/button_group */} - {needsOptions && ( - - -
-
- - Варианты выбора -
- -
-
- -
- {formData.options?.map((option, index) => ( -
- - handleUpdateOption(index, 'value', e.target.value) - } - className="h-8 text-xs" - /> - - handleUpdateOption(index, 'label', e.target.value) - } - className="h-8 text-xs" - /> - -
- ))} - {(!formData.options || formData.options.length === 0) && ( -
- -

- Добавьте варианты выбора -

-
- )} -
-
-
- )}
{/* Предпросмотр */} diff --git a/src/component/VerificationConditionsElement/VerificationConditionsEditor.tsx b/src/component/VerificationConditionsElement/VerificationConditionsEditor.tsx index 2c5a37c..4439027 100644 --- a/src/component/VerificationConditionsElement/VerificationConditionsEditor.tsx +++ b/src/component/VerificationConditionsElement/VerificationConditionsEditor.tsx @@ -1,5 +1,4 @@ import { Badge } from '@/component/ui/badge' -import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card' import { Label } from '@/component/ui/label' import { Select, @@ -8,13 +7,13 @@ import { SelectTrigger, SelectValue, } from '@/component/ui/select' -import { Building2, Target } from 'lucide-react' -import { useLaboratories } from './hooks' -import { mockApiHelpers } from './mockData' -import { ConditionCellMapping, VerificationConditionsConfig } from './types' +import { CellTarget } from '@/type/template' +import { Building2 } from 'lucide-react' +import { useLaboratories, useLaboratoryConditions } from './hooks' +import { ConditionMapping, VerificationConditionsConfig } from './types' interface VerificationConditionsEditorProps { - config: VerificationConditionsConfig + config: VerificationConditionsConfig & { targetCells?: CellTarget[] } onChange: (config: VerificationConditionsConfig) => void } @@ -23,204 +22,198 @@ export function VerificationConditionsEditor({ onChange, }: VerificationConditionsEditorProps) { const { data: laboratories = [], isLoading } = useLaboratories() + const { data: labData } = useLaboratoryConditions(config.selectedLaboratoryId) const updateConfig = (updates: Partial) => { onChange({ ...config, ...updates }) } - // Получаем выбранную лабораторию - const selectedLab = laboratories.find( - lab => lab.id === config.selectedLaboratoryId - ) + // Получить доступные целевые ячейки из конфигурации + const getAvailableTargetCells = () => { + return config.targetCells || [] + } - // Получаем все доступные условия (либо все из БД, либо условия конкретной лаборатории) - const availableConditions = config.selectedLaboratoryId - ? mockApiHelpers.getLaboratoryConditions(config.selectedLaboratoryId) - : mockApiHelpers.getAllConditions() + // Обновить маппинг для конкретного условия + const updateConditionMapping = ( + conditionId: string, + conditionName: string, + targetCellKey?: string // формат: "sheet:cell", например "L:A1" + ) => { + const currentMappings = config.conditionMappings || [] + let updatedMappings = [...currentMappings] - // Обновление привязки условия к ячейке - const updateConditionMapping = (cellIndex: number, conditionKey: string) => { - const conditionName = - availableConditions.find(c => c.key === conditionKey)?.name || - conditionKey + // Удаляем существующий маппинг для этого условия + updatedMappings = updatedMappings.filter( + mapping => mapping.conditionKey !== conditionId + ) - const newMappings = [...(config.conditionMappings || [])] - const existingIndex = newMappings.findIndex(m => m.cellIndex === cellIndex) + // Если выбрана ячейка, добавляем новый маппинг + if (targetCellKey) { + const [sheet, cell] = targetCellKey.split(':') + const targetCells = getAvailableTargetCells() + const targetIndex = targetCells.findIndex( + tc => tc.sheet === sheet && tc.cell === cell + ) - if (conditionKey === '__none__') { - // Убираем привязку - if (existingIndex >= 0) { - newMappings.splice(existingIndex, 1) - } - } else { - // Добавляем или обновляем привязку - const mapping: ConditionCellMapping = { - cellIndex, - conditionKey, - conditionName, - } - - if (existingIndex >= 0) { - newMappings[existingIndex] = mapping - } else { - newMappings.push(mapping) + if (targetIndex !== -1) { + const newMapping: ConditionMapping = { + conditionKey: conditionId, + conditionName: conditionName, + cellIndex: targetIndex, // используем индекс в массиве targetCells + } + updatedMappings.push(newMapping) } } - updateConfig({ conditionMappings: newMappings }) + updateConfig({ conditionMappings: updatedMappings }) } - // Получить выбранное условие для ячейки - const getSelectedCondition = (cellIndex: number): string => { - const mapping = config.conditionMappings?.find( - m => m.cellIndex === cellIndex + // Получить выбранную ячейку для условия + const getSelectedTargetCell = (conditionId: string): string | undefined => { + const mapping = (config.conditionMappings || []).find( + mapping => mapping.conditionKey === conditionId ) - return mapping?.conditionKey || '__none__' + if (!mapping) return undefined + + const targetCells = getAvailableTargetCells() + const targetCell = targetCells[mapping.cellIndex] + return targetCell ? `${targetCell.sheet}:${targetCell.cell}` : undefined } + // Получить занятые ячейки (исключая текущее условие) + const getOccupiedTargetCells = (excludeConditionId?: string): string[] => { + const targetCells = getAvailableTargetCells() + return (config.conditionMappings || []) + .filter(mapping => mapping.conditionKey !== excludeConditionId) + .map(mapping => { + const targetCell = targetCells[mapping.cellIndex] + return targetCell ? `${targetCell.sheet}:${targetCell.cell}` : '' + }) + .filter(Boolean) + } + + const targetCells = getAvailableTargetCells() + return (
- {/* Выбор лаборатории */} - - - - - Лаборатория - - - -
- - -

- {config.selectedLaboratoryId - ? 'Доступны только условия выбранной лаборатории' - : 'Доступны все условия из базы данных'} -

-
-
-
+ {/* Компактный выбор лаборатории */} +
+
+ + +
+
+ +
+
- {/* Привязка условий к ячейкам */} - {config.targetCells.length > 0 && ( - - - - - Привязка условий к ячейкам - - - -
-

Настройка записи данных

-

- Выберите какое условие будет записываться в каждую целевую - ячейку. -

-
+ {/* Маппинг условий */} + {labData && targetCells.length > 0 && ( +
+ +
+ {labData.conditionTypes.map(conditionType => { + const selectedCell = getSelectedTargetCell(conditionType.id) + const occupiedCells = getOccupiedTargetCells(conditionType.id) -
- {config.targetCells.map((cell, index) => ( + return (
- - {cell.sheet}!{cell.cell} - + {/* Информация об условии */} +
+ + {conditionType.name} + + + {conditionType.unit} + +
-
- - + updateConditionMapping( + conditionType.id, + conditionType.name, + value === 'UNSELECTED' ? undefined : value + ) + } + > + + + + + + {targetCells + .map( + (target, index) => `${target.sheet}:${target.cell}` + ) + .filter(cellKey => !occupiedCells.includes(cellKey)) + .map(cellKey => ( + + {cellKey} ))} - - -
+ +
- ))} -
+ ) + })} - {/* Подсказка с доступными условиями */} -
-

- Доступные условия{' '} - {selectedLab ? `для ${selectedLab.name}` : 'из базы данных'}: -

-
    - {availableConditions.map(condition => ( -
  • - • {condition.name} - {condition.unit && ` (${condition.unit})`} - {'isRequired' in condition && - condition.isRequired && - ' - обязательное'} -
  • - ))} -
-
- - + {labData.conditionTypes.length === 0 && ( +
+

+ У выбранной лаборатории нет настроенных типов условий +

+
+ )} +
+
)} - {/* Подсказка если нет целевых ячеек */} - {config.targetCells.length === 0 && ( - - -
- -

Целевые ячейки не настроены

-

- Сначала настройте целевые ячейки в основном редакторе элементов, - затем вернитесь сюда для привязки условий. -

-
-
-
+ {/* Предупреждение если нет целевых ячеек */} + {targetCells.length === 0 && ( +
+

+ Добавьте целевые ячейки в настройки элемента для маппинга условий + поверки +

+
+ )} + + {/* Предупреждение если выбрана лаборатория но нет ячеек */} + {labData && targetCells.length === 0 && ( +
+

+ Настройте целевые ячейки для сопоставления условий поверки +

+
)}
) diff --git a/src/component/VerificationConditionsElement/VerificationConditionsPreview.tsx b/src/component/VerificationConditionsElement/VerificationConditionsPreview.tsx index 627b6c2..02207d7 100644 --- a/src/component/VerificationConditionsElement/VerificationConditionsPreview.tsx +++ b/src/component/VerificationConditionsElement/VerificationConditionsPreview.tsx @@ -1,129 +1,182 @@ -import { Badge } from '@/component/ui/badge' +import { Button } from '@/component/ui/button' +import { Calendar } from '@/component/ui/calendar' import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card' -import { Building2, Calendar, Droplets, Gauge, Thermometer } from 'lucide-react' +import { Popover, PopoverContent, PopoverTrigger } from '@/component/ui/popover' +import { + AlertCircle, + Building2, + Calendar as CalendarIcon, + CheckCircle, + Plus, +} from 'lucide-react' +import { useState } from 'react' +import { + useDailyConditions, + useLaboratories, + useLaboratoryConditions, +} from './hooks' import { VerificationConditionsConfig } from './types' interface VerificationConditionsPreviewProps { config: VerificationConditionsConfig -} - -// Моковые данные для предварительного просмотра -const mockData = { - laboratory: { - id: 1, - name: 'Лаборатория температуры', - code: 'TEMP_LAB', - }, - date: '2024-01-15', - conditions: [ - { - typeName: 'Температура', - value: 23.5, - unit: '°C', - icon: , - }, - { - typeName: 'Влажность', - value: 45.2, - unit: '%', - icon: , - }, - { - typeName: 'Атмосферное давление', - value: 101.3, - unit: 'кПа', - icon: , - }, - ], + onChange?: (config: VerificationConditionsConfig) => void } export function VerificationConditionsPreview({ config, + onChange, }: VerificationConditionsPreviewProps) { - const displayedConditions = mockData.conditions.slice(0, config.maxConditions) + const [selectedDate, setSelectedDate] = useState( + config.selectedDate || new Date().toISOString().split('T')[0] + ) + const [calendarOpen, setCalendarOpen] = useState(false) + + const { data: laboratories = [] } = useLaboratories() + const { data: labData } = useLaboratoryConditions(config.selectedLaboratoryId) + const { data: dailyCondition, isLoading } = useDailyConditions( + config.selectedLaboratoryId, + selectedDate + ) + + const selectedLab = laboratories.find( + lab => lab.id === config.selectedLaboratoryId + ) + + const hasConditions = + dailyCondition && Object.keys(dailyCondition.conditions).length > 0 + + const handleDateChange = (date: string) => { + setSelectedDate(date) + if (onChange) { + onChange({ ...config, selectedDate: date }) + } + } + + const handleCalendarSelect = (date: Date | undefined) => { + if (date) { + const dateString = date.toISOString().split('T')[0] + setSelectedDate(dateString) + setCalendarOpen(false) + if (onChange) { + onChange({ ...config, selectedDate: dateString }) + } + } + } + + if (!config.selectedLaboratoryId) { + return ( + + +
+ +

Лаборатория не выбрана

+
+
+
+ ) + } + + if (!labData) { + return ( + + +
+
+

Загрузка...

+
+ + + ) + } return ( - - - Условия поверки + + + + + {selectedLab?.name || 'Лаборатория'} + - - {/* Селекторы лаборатории и даты */} -
-
-
- - Лаборатория -
-
- {config.selectedLaboratoryId - ? mockData.laboratory.name - : 'Не выбрана'} -
-
-
-
- - Дата -
-
- {config.selectedDate || mockData.date} -
-
-
- - {/* Условия */} -
-
- Условия -
-
- {displayedConditions.map((condition, index) => ( -
+ {/* Выбор даты */} +
+ + + +
- ))} - - {mockData.conditions.length > config.maxConditions && ( -
- ... и ещё {mockData.conditions.length - config.maxConditions}{' '} - условий -
- )} -
+ {new Date(selectedDate).toLocaleDateString('ru-RU')} + + + + + +
- {/* Настройки */} -
-
-
- Лист: {config.sheet} + {/* Статус и данные */} +
+ {isLoading ? ( +
+
+ Загрузка...
-
- Ячейка: {config.startCell} + ) : hasConditions ? ( +
+
+ + Данные внесены +
+ +
+ {labData.conditionTypes.map(conditionType => { + const conditionValue = + dailyCondition!.conditions[conditionType.id] + return ( +
+ + {conditionType.name} + + + {conditionValue || '—'} + {conditionValue && conditionType.unit && ( + {conditionType.unit} + )} + +
+ ) + })} +
-
Макс. условий: {config.maxConditions}
- {config.autoSave &&
✓ Автосохранение
} - {config.showUnits &&
✓ Показывать единицы
} - {config.targetCells.length > 0 && ( -
Ячеек настроено: {config.targetCells.length}
- )} -
+ ) : ( +
+
+ + Данные не внесены +
+ + +
+ )}
diff --git a/src/component/VerificationConditionsElement/VerificationConditionsRender.tsx b/src/component/VerificationConditionsElement/VerificationConditionsRender.tsx index b0b0699..f2aa78a 100644 --- a/src/component/VerificationConditionsElement/VerificationConditionsRender.tsx +++ b/src/component/VerificationConditionsElement/VerificationConditionsRender.tsx @@ -1,30 +1,30 @@ -import { Alert, AlertDescription } from '@/component/ui/alert' -import { Badge } from '@/component/ui/badge' import { Button } from '@/component/ui/button' +import { Calendar } from '@/component/ui/calendar' import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/component/ui/dialog' import { Input } from '@/component/ui/input' import { Label } from '@/component/ui/label' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/component/ui/select' +import { Popover, PopoverContent, PopoverTrigger } from '@/component/ui/popover' import { AlertCircle, Building2, - Calendar, - CheckCircle2, - Droplets, - Gauge, - Loader2, - Save, - Thermometer, - Zap, + Calendar as CalendarIcon, + CheckCircle, + Plus, + Settings, } from 'lucide-react' -import { useEffect } from 'react' -import { useVerificationConditionsElement } from './hooks' +import { useEffect, useState } from 'react' +import { + useDailyConditions, + useLaboratories, + useLaboratoryConditions, +} from './hooks' import { VerificationConditionsConfig, VerificationConditionsValue, @@ -34,277 +34,352 @@ interface VerificationConditionsRenderProps { config: VerificationConditionsConfig value?: VerificationConditionsValue onChange?: (value: VerificationConditionsValue) => void -} - -// Иконки для разных типов условий -const getConditionIcon = (conditionName: string) => { - const name = conditionName.toLowerCase() - if (name.includes('температур')) return - if (name.includes('влажн')) return - if (name.includes('давлен')) return - if (name.includes('напряжен') || name.includes('частот')) - return - return
+ onSave?: (value: VerificationConditionsValue) => Promise } export function VerificationConditionsRender({ config, value, onChange, + onSave, }: VerificationConditionsRenderProps) { + const [selectedDate, setSelectedDate] = useState( + config.selectedDate || new Date().toISOString().split('T')[0] + ) + const [editDialogOpen, setEditDialogOpen] = useState(false) + const [calendarOpen, setCalendarOpen] = useState(false) + const [editingConditions, setEditingConditions] = useState< + Record + >({}) + + const { data: laboratories = [] } = useLaboratories() + const { data: labData } = useLaboratoryConditions(config.selectedLaboratoryId) const { - laboratories, - selectedLaboratory, - selectedLaboratoryId, - selectedDate, - conditions, - localConditions, - isLoading, - isError, - isSaving, - hasUnsavedChanges, - validationErrors, - changeLaboratory, - changeDate, - updateCondition, + data: dailyCondition, saveConditions, - getCurrentValue, - formatValueWithUnit, - getConditionKey, - } = useVerificationConditionsElement({ - selectedLaboratoryId: config.selectedLaboratoryId || value?.laboratoryId, - selectedDate: config.selectedDate || value?.date, - autoSave: config.autoSave, - }) + isSaving, + } = useDailyConditions(config.selectedLaboratoryId, selectedDate) - // Синхронизируем значение с родительским компонентом + const selectedLab = laboratories.find( + lab => lab.id === config.selectedLaboratoryId + ) + + const hasConditions = + dailyCondition && Object.keys(dailyCondition.conditions).length > 0 + + // Автоматическая инициализация onChange при загрузке данных useEffect(() => { - if (onChange) { - const currentValue = getCurrentValue() - if ( - currentValue && - (!value || - currentValue.laboratoryId !== value.laboratoryId || - currentValue.date !== value.date || - JSON.stringify(currentValue.conditions) !== - JSON.stringify(value.conditions)) - ) { - onChange(currentValue) - } + if (dailyCondition && onChange && hasConditions) { + onChange({ + date: selectedDate, + conditions: dailyCondition.conditions, + }) } - }, [onChange, getCurrentValue, value]) + }, [dailyCondition, onChange, selectedDate, hasConditions]) + + const handleDateChange = (date: string) => { + setSelectedDate(date) + } + + const handleCalendarSelect = (date: Date | undefined) => { + if (date) { + const dateString = date.toISOString().split('T')[0] + setSelectedDate(dateString) + setCalendarOpen(false) + } + } + + const handleOpenEditDialog = (isCreating = false) => { + if (isCreating) { + const emptyConditions: Record = {} + labData?.conditionTypes.forEach(conditionType => { + emptyConditions[conditionType.id] = '' + }) + setEditingConditions(emptyConditions) + } else { + setEditingConditions(dailyCondition?.conditions || {}) + } + setEditDialogOpen(true) + } + + const handleSaveConditions = async () => { + if (!config.selectedLaboratoryId || !selectedDate || !labData) return - // Обработка сохранения - const handleSave = async () => { try { - await saveConditions() + await saveConditions(editingConditions) + + if (onChange) { + onChange({ + date: selectedDate, + conditions: editingConditions, + }) + } + + setEditDialogOpen(false) } catch (error) { console.error('Ошибка сохранения:', error) } } - // Получение ошибки валидации для поля - const getFieldError = (fieldKey: string) => { - return validationErrors.find(error => error.field === fieldKey) + const handleConditionChange = (conditionId: string, value: string) => { + setEditingConditions(prev => ({ + ...prev, + [conditionId]: value, + })) } - if (isLoading) { + if (!config.selectedLaboratoryId) { return ( - - - - Загрузка... + + +
+ +

Лаборатория не настроена

+
) } - if (isError) { + if (!labData) { return ( - - - - Ошибка загрузки данных условий поверки - - + + +
+
+

Загрузка...

+
+ + ) } return ( - - - - Условия поверки - {hasUnsavedChanges && !config.autoSave && ( - - )} + + + + + {selectedLab?.name || 'Лаборатория'} - - {/* Селекторы лаборатории и даты */} -
-
- - -
-
- - changeDate(e.target.value)} - className="mt-1" - /> -
+ + {/* Выбор даты */} +
+ + + + + + + + +
- {/* Условия */} - {conditions && selectedLaboratoryId && selectedDate && ( -
- -
- {conditions.conditions - .slice(0, config.maxConditions) - .map((condition, index) => { - const key = getConditionKey(condition.typeName) - const error = getFieldError(key) + {/* Статус и данные */} +
+ {hasConditions ? ( +
+
+
+ + Данные внесены +
- return ( -
+ + + + + + + Условия поверки + + +
+
+
{selectedLab?.name}
+
+ {new Date(selectedDate).toLocaleDateString('ru-RU')}
-
- updateCondition(key, e.target.value)} - placeholder={ - condition.minValue && condition.maxValue - ? `${condition.minValue}-${condition.maxValue}` - : 'Значение' - } - className={`w-20 text-right ${error ? 'border-destructive' : ''}`} - /> - {condition.isRequired && ( - * - )} +
+ {labData.conditionTypes.map(conditionType => ( +
+ + + handleConditionChange( + conditionType.id, + e.target.value + ) + } + placeholder="Значение" + className="h-7 text-xs" + /> +
+ ))}
+ +
+ + +
+
+ + +
+ +
+ {labData.conditionTypes.map(conditionType => { + const conditionValue = + dailyCondition!.conditions[conditionType.id] + return ( +
+ + {conditionType.name} + + + {conditionValue || '—'} + {conditionValue && conditionType.unit && ( + {conditionType.unit} + )} +
) })} - - {conditions.conditions.length > config.maxConditions && ( -
- ... и ещё{' '} - {conditions.conditions.length - config.maxConditions} условий -
- )} +
-
- )} + ) : ( +
+
+ + Данные не внесены +
- {/* Ошибки валидации */} - {validationErrors.length > 0 && ( - - - -
- {validationErrors.map((error, index) => ( -
- {error.message} + + + + + + + + Условия поверки + + +
+
+
{selectedLab?.name}
+
+ {new Date(selectedDate).toLocaleDateString('ru-RU')} +
+
+ +
+ {labData.conditionTypes.map(conditionType => ( +
+ + + handleConditionChange( + conditionType.id, + e.target.value + ) + } + placeholder="Значение" + className="h-7 text-xs" + /> +
+ ))} +
+ +
+ + +
- ))} -
- - - )} - - {/* Статус */} -
-
- {config.autoSave && hasUnsavedChanges && ( -
- - Автосохранение... -
- )} - {config.autoSave && - !hasUnsavedChanges && - selectedLaboratoryId && - selectedDate && ( -
- - Сохранено -
- )} -
- - {selectedLaboratory &&
{selectedLaboratory.name}
} + + +
+ )}
- - {/* Подсказка для пустого состояния */} - {!selectedLaboratoryId && ( -
- -

Выберите лабораторию для настройки условий поверки

-
- )} ) diff --git a/src/component/VerificationConditionsElement/api.ts b/src/component/VerificationConditionsElement/api.ts index b10efe7..04cc328 100644 --- a/src/component/VerificationConditionsElement/api.ts +++ b/src/component/VerificationConditionsElement/api.ts @@ -1,165 +1,129 @@ import { ConditionType, - DailyConditions, - ElementConditionsData, + CreateDailyConditionInput, + CreateDailyConditionOutput, + DailyCondition, + DailyConditionsFilters, + GetDailyConditionOutput, + GetDailyConditionsOutput, + GetLaboratoriesOutput, + GetLaboratoryOutput, Laboratory, - SaveConditionsResponse, - VerificationConditionsValue, + UpdateDailyConditionInput, + UpdateDailyConditionOutput, } from './types' -const BASE_URL = '/api/v1/verification-conditions' +// Базовый URL для API (может быть настроен через переменные окружения) +const BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api' // API для лабораторий export const laboratoriesApi = { // Получить все лаборатории getAll: async (): Promise => { - const response = await fetch(`${BASE_URL}/laboratories`) + const response = await fetch(`${BASE_URL}/laboratories/`) if (!response.ok) { throw new Error('Ошибка загрузки лабораторий') } - return response.json() + const data: GetLaboratoriesOutput = await response.json() + return data.laboratories }, // Получить лабораторию по ID - getById: async (id: number): Promise => { + getById: async (id: string): Promise => { const response = await fetch(`${BASE_URL}/laboratories/${id}`) if (!response.ok) { - throw new Error('Лаборатория не найдена') + if (response.status === 404) { + return null + } + throw new Error('Ошибка получения лаборатории') } - return response.json() - }, - - // Создать лабораторию - create: async ( - data: Omit - ): Promise => { - const response = await fetch(`${BASE_URL}/laboratories`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }) - if (!response.ok) { - throw new Error('Ошибка создания лаборатории') - } - return response.json() - }, -} - -// API для типов условий -export const conditionTypesApi = { - // Получить все типы условий - getAll: async (): Promise => { - const response = await fetch(`${BASE_URL}/condition-types`) - if (!response.ok) { - throw new Error('Ошибка загрузки типов условий') - } - return response.json() - }, - - // Создать тип условия - create: async ( - data: Omit - ): Promise => { - const response = await fetch(`${BASE_URL}/condition-types`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }) - if (!response.ok) { - throw new Error('Ошибка создания типа условия') - } - return response.json() + const data: GetLaboratoryOutput = await response.json() + return data.laboratory }, } // API для ежедневных условий export const dailyConditionsApi = { // Получить условия по фильтрам - get: async (params: { - laboratoryId?: number - date?: string - dateFrom?: string - dateTo?: string - }): Promise => { + get: async ( + filters: DailyConditionsFilters = {} + ): Promise => { const searchParams = new URLSearchParams() - Object.entries(params).forEach(([key, value]) => { - if (value) searchParams.append(key, value.toString()) - }) + if (filters.laboratory_id) { + searchParams.append('laboratory_id', filters.laboratory_id) + } + if (filters.start_date) { + searchParams.append('start_date', filters.start_date) + } + if (filters.end_date) { + searchParams.append('end_date', filters.end_date) + } - const response = await fetch(`${BASE_URL}/daily-conditions?${searchParams}`) + const url = searchParams.toString() + ? `${BASE_URL}/daily-conditions/?${searchParams}` + : `${BASE_URL}/daily-conditions/` + + const response = await fetch(url) if (!response.ok) { throw new Error('Ошибка загрузки условий') } - return response.json() + const data: GetDailyConditionsOutput = await response.json() + return data.daily_conditions }, - // Получить условия для элемента - getForElement: async ( - laboratoryId: number, - date: string - ): Promise => { - const response = await fetch( - `${BASE_URL}/daily-conditions/for-element?laboratoryId=${laboratoryId}&date=${date}` - ) + // Получить конкретное условие по ID + getById: async (id: string): Promise => { + const response = await fetch(`${BASE_URL}/daily-conditions/${id}`) if (!response.ok) { - throw new Error('Ошибка загрузки условий для элемента') - } - return response.json() - }, - - // Сохранить условия из элемента - saveForElement: async ( - data: VerificationConditionsValue - ): Promise => { - const response = await fetch( - `${BASE_URL}/daily-conditions/save-for-element`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), + if (response.status === 404) { + return null } - ) - if (!response.ok) { - const error = await response.json() - throw new Error(error.message || 'Ошибка сохранения условий') + throw new Error('Ошибка получения условия') } - return response.json() + const data: GetDailyConditionOutput = await response.json() + return data.daily_condition }, - // Создать/обновить условия - save: async ( - data: Omit - ): Promise => { - const response = await fetch(`${BASE_URL}/daily-conditions`, { + // Создать новые условия + create: async (input: CreateDailyConditionInput): Promise => { + const response = await fetch(`${BASE_URL}/daily-conditions/`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(input), }) if (!response.ok) { - const error = await response.json() + const error = await response + .json() + .catch(() => ({ message: 'Ошибка сохранения условий' })) throw new Error(error.message || 'Ошибка сохранения условий') } - return response.json() + const data: CreateDailyConditionOutput = await response.json() + return data.id }, // Обновить условия update: async ( - id: number, - data: Partial - ): Promise => { + id: string, + input: UpdateDailyConditionInput + ): Promise => { const response = await fetch(`${BASE_URL}/daily-conditions/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(input), }) if (!response.ok) { throw new Error('Ошибка обновления условий') } - return response.json() + const data: UpdateDailyConditionOutput = await response.json() + return data.id }, // Удалить условия - delete: async (id: number): Promise => { + delete: async (id: string): Promise => { const response = await fetch(`${BASE_URL}/daily-conditions/${id}`, { method: 'DELETE', }) @@ -169,12 +133,83 @@ export const dailyConditionsApi = { }, } +// Комбинированный API для работы с элементом +export const elementApi = { + // Получить лабораторию с её типами условий + getLaboratoryData: async ( + laboratoryId: string + ): Promise<{ + laboratory: Laboratory + conditionTypes: ConditionType[] + }> => { + const laboratory = await laboratoriesApi.getById(laboratoryId) + if (!laboratory) { + throw new Error('Лаборатория не найдена') + } + + return { + laboratory, + conditionTypes: laboratory.condition_types, + } + }, + + // Получить ежедневные условия для конкретной лаборатории и даты + getDailyConditions: async ( + laboratoryId: string, + date: string + ): Promise => { + const conditions = await dailyConditionsApi.get({ + laboratory_id: laboratoryId, + start_date: date, + end_date: date, + }) + return conditions.length > 0 ? conditions[0] : null + }, + + // Сохранить или обновить условия из элемента + saveElementConditions: async (data: { + laboratoryId: string + date: string + conditions: Record + existingConditionId?: string + }): Promise => { + const input: CreateDailyConditionInput | UpdateDailyConditionInput = { + laboratory_id: data.laboratoryId, + measurement_date: data.date, + conditions: data.conditions, + } + + let conditionId: string + + if (data.existingConditionId) { + // Обновляем существующие условия + conditionId = await dailyConditionsApi.update( + data.existingConditionId, + input as UpdateDailyConditionInput + ) + } else { + // Создаем новые условия + conditionId = await dailyConditionsApi.create( + input as CreateDailyConditionInput + ) + } + + // Получаем обновленные данные + const savedCondition = await dailyConditionsApi.getById(conditionId) + if (!savedCondition) { + throw new Error('Ошибка получения сохраненных условий') + } + + return savedCondition + }, +} + // Вспомогательные функции export const verificationConditionsUtils = { // Валидация значения условия validateConditionValue: ( value: any, - dataType: string, + dataType: string = 'string', minValue?: number, maxValue?: number ): { isValid: boolean; error?: string } => { @@ -201,11 +236,23 @@ export const verificationConditionsUtils = { return unit ? `${value} ${unit}` : value.toString() }, - // Получение ключа условия для хранения в conditions JSON - getConditionKey: (conditionTypeName: string): string => { + // Получение ключа условия для хранения в conditions JSON (обычно используется ID типа условия) + getConditionKey: (conditionTypeId: string): string => { + return conditionTypeId + }, + + // Генерация ключа на основе названия условия (для обратной совместимости) + generateConditionKey: (conditionTypeName: string): string => { return conditionTypeName .toLowerCase() .replace(/\s+/g, '_') .replace(/[^\w]/g, '') }, + + // Проверка, является ли строка валидным UUID + isValidUUID: (str: string): boolean => { + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + return uuidRegex.test(str) + }, } diff --git a/src/component/VerificationConditionsElement/definition.tsx b/src/component/VerificationConditionsElement/definition.tsx index 90b1f22..0b994b1 100644 --- a/src/component/VerificationConditionsElement/definition.tsx +++ b/src/component/VerificationConditionsElement/definition.tsx @@ -1,6 +1,6 @@ import { ElementDefinition } from '@/entitiy/element/model/interface' import { CellTarget } from '@/type/template' -import { Gauge } from 'lucide-react' +import { Building2 } from 'lucide-react' import { VerificationConditionsConfig, VerificationConditionsValue, @@ -9,70 +9,68 @@ import { VerificationConditionsEditor } from './VerificationConditionsEditor' import { VerificationConditionsPreview } from './VerificationConditionsPreview' import { VerificationConditionsRender } from './VerificationConditionsRender' -export const verificationConditionsDefinition: ElementDefinition< - VerificationConditionsConfig, +export const verificationConditionsElementDefinition: ElementDefinition< + VerificationConditionsConfig & { targetCells?: CellTarget[] }, VerificationConditionsValue > = { type: 'verification_conditions', label: 'Условия поверки', - icon: , + icon: , version: 1, defaultConfig: { - targetCells: [], + selectedLaboratoryId: undefined, + selectedDate: undefined, conditionMappings: [], + showUnits: true, + targetCells: [], }, + // Компоненты Editor: VerificationConditionsEditor, Preview: VerificationConditionsPreview, Render: VerificationConditionsRender, - mapToCells: config => { - // Возвращаем настроенные пользователем целевые ячейки - return config.targetCells.map(cell => ({ - sheet: cell.sheet, - cell: cell.cell, - })) + // Получение ячеек для записи данных на основе маппинга условий + mapToCells: (config): CellTarget[] => { + // Возвращаем только те targetCells, которые замаплены к условиям + if (!config.conditionMappings || !config.targetCells) { + return [] + } + + return config.conditionMappings + .map(mapping => config.targetCells?.[mapping.cellIndex]) + .filter(Boolean) as CellTarget[] }, - mapToCellValues: (config, value) => { - if (!value) return [] + // Получение значения для записи в ячейки на основе реального маппинга + mapToCellValues: ( + config, + value: VerificationConditionsValue + ): Array<{ target: CellTarget; value: any }> => { + if (!config.conditionMappings || !config.targetCells || !value.conditions) { + return [] + } - const targetCells = config.targetCells || [] - const mappings = config.conditionMappings || [] - if (targetCells.length === 0) return [] - - const results: Array<{ target: CellTarget; value: any }> = [] - - // Записываем данные в ячейки согласно настроенным привязкам - mappings.forEach(mapping => { - const targetCell = targetCells[mapping.cellIndex] - if (!targetCell) return - - let cellValue: any = '' - - switch (mapping.conditionKey) { - case 'laboratory': - cellValue = `Лаборатория ${value.laboratoryId}` // Здесь можно подставить реальное название - break - case 'date': - cellValue = value.date - break - default: - // Это условие из лаборатории - cellValue = value.conditions[mapping.conditionKey] || '' - break - } - - results.push({ - target: { - sheet: targetCell.sheet, - cell: targetCell.cell, - }, - value: cellValue, + return config.conditionMappings + .filter(mapping => { + const conditionValue = value.conditions[mapping.conditionKey] + // Записываем только непустые значения + return ( + conditionValue !== undefined && + conditionValue !== null && + String(conditionValue).trim() !== '' + ) }) - }) + .map(mapping => { + const targetCell = config.targetCells?.[mapping.cellIndex] + if (!targetCell) return null - return results + return { + target: targetCell, + value: value.conditions[mapping.conditionKey], + } + }) + .filter(Boolean) as Array<{ target: CellTarget; value: any }> }, } diff --git a/src/component/VerificationConditionsElement/hooks.ts b/src/component/VerificationConditionsElement/hooks.ts index bcd2a86..76c8757 100644 --- a/src/component/VerificationConditionsElement/hooks.ts +++ b/src/component/VerificationConditionsElement/hooks.ts @@ -1,13 +1,8 @@ import { useCallback, useEffect, useState } from 'react' -import { mockApiHelpers, mockLaboratories } from './mockData' -import { - ElementConditionsData, - Laboratory, - ValidationError, - VerificationConditionsValue, -} from './types' +import { dailyConditionsApi, elementApi, laboratoriesApi } from './api' +import { ConditionType, DailyCondition, Laboratory } from './types' -// Простой хук для загрузки лабораторий (используем моковые данные) +// Хук для загрузки лабораторий export function useLaboratories() { const [data, setData] = useState([]) const [isLoading, setIsLoading] = useState(true) @@ -17,9 +12,9 @@ export function useLaboratories() { const loadData = async () => { try { setIsLoading(true) - await mockApiHelpers.delay(300) // Симуляция загрузки - setData(mockLaboratories) setIsError(false) + const laboratories = await laboratoriesApi.getAll() + setData(laboratories) } catch (error) { setIsError(true) console.error('Ошибка загрузки лабораторий:', error) @@ -34,25 +29,127 @@ export function useLaboratories() { return { data, isLoading, isError } } -// Хук для загрузки конкретной лаборатории -export function useLaboratory(id: number | undefined) { - const [data, setData] = useState() +// Хук для работы с ежедневными условиями +export function useDailyConditions(laboratoryId?: string, date?: string) { + const [data, setData] = useState(null) const [isLoading, setIsLoading] = useState(false) const [isError, setIsError] = useState(false) + const [isSaving, setIsSaving] = useState(false) useEffect(() => { - if (!id) { - setData(undefined) + if (!laboratoryId || !date) { + setData(null) return } const loadData = async () => { try { setIsLoading(true) - await mockApiHelpers.delay(200) - const laboratory = mockApiHelpers.getLaboratoryById(id) + setIsError(false) + const conditions = await elementApi.getDailyConditions( + laboratoryId, + date + ) + setData(conditions) + } catch (error) { + setIsError(true) + console.error('Ошибка загрузки ежедневных условий:', error) + } finally { + setIsLoading(false) + } + } + + loadData() + }, [laboratoryId, date]) + + const saveConditions = useCallback( + async (conditions: Record) => { + if (!laboratoryId || !date) { + throw new Error('Не указаны лаборатория или дата') + } + + try { + setIsSaving(true) + const savedCondition = await elementApi.saveElementConditions({ + laboratoryId, + date, + conditions, + existingConditionId: data?.id, + }) + setData(savedCondition) + return savedCondition + } catch (error) { + console.error('Ошибка сохранения условий:', error) + throw error + } finally { + setIsSaving(false) + } + }, + [laboratoryId, date, data?.id] + ) + + return { + data, + isLoading, + isError, + isSaving, + saveConditions, + } +} + +// Хук для получения данных лаборатории с типами условий +export function useLaboratoryConditions(laboratoryId?: string) { + const [data, setData] = useState<{ + laboratory: Laboratory + conditionTypes: ConditionType[] + } | null>(null) + const [isLoading, setIsLoading] = useState(false) + const [isError, setIsError] = useState(false) + + useEffect(() => { + if (!laboratoryId) { + setData(null) + return + } + + const loadData = async () => { + try { + setIsLoading(true) + setIsError(false) + const result = await elementApi.getLaboratoryData(laboratoryId) + setData(result) + } catch (error) { + setIsError(true) + console.error('Ошибка загрузки условий лаборатории:', error) + } finally { + setIsLoading(false) + } + } + + loadData() + }, [laboratoryId]) + + return { data, isLoading, isError } +} + +// Хук для получения конкретной лаборатории +export function useLaboratory(laboratoryId?: string) { + const [data, setData] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [isError, setIsError] = useState(false) + + useEffect(() => { + if (!laboratoryId) { + setData(null) + return + } + + const loadData = async () => { + try { + setIsLoading(true) + setIsError(false) + const laboratory = await laboratoriesApi.getById(laboratoryId) setData(laboratory) - setIsError(!laboratory) } catch (error) { setIsError(true) console.error('Ошибка загрузки лаборатории:', error) @@ -62,370 +159,60 @@ export function useLaboratory(id: number | undefined) { } loadData() - }, [id]) + }, [laboratoryId]) return { data, isLoading, isError } } -// Хук для загрузки условий для элемента -export function useElementConditions( - laboratoryId: number | undefined, - date: string | undefined -) { - const [data, setData] = useState() +// Хук для управления ежедневными условиями с расширенным функционалом +export function useDailyConditionsManager() { const [isLoading, setIsLoading] = useState(false) const [isError, setIsError] = useState(false) - useEffect(() => { - if (!laboratoryId || !date) { - setData(undefined) - return - } - - const loadData = async () => { + const loadConditions = useCallback( + async (filters: { + laboratoryId?: string + startDate?: string + endDate?: string + }) => { try { setIsLoading(true) - await mockApiHelpers.delay(400) - const conditions = mockApiHelpers.getElementConditions( - laboratoryId, - date - ) - setData(conditions) - setIsError(!conditions) + setIsError(false) + const conditions = await dailyConditionsApi.get({ + laboratory_id: filters.laboratoryId, + start_date: filters.startDate, + end_date: filters.endDate, + }) + return conditions } catch (error) { setIsError(true) console.error('Ошибка загрузки условий:', error) + throw error } finally { setIsLoading(false) } - } - - loadData() - }, [laboratoryId, date]) - - return { data, isLoading, isError } -} - -// Хук для сохранения условий -export function useSaveConditions() { - const [isPending, setIsPending] = useState(false) - const [isError, setIsError] = useState(false) - const [error, setError] = useState(null) - - const mutateAsync = useCallback( - async (value: VerificationConditionsValue) => { - try { - setIsPending(true) - setIsError(false) - setError(null) - - await mockApiHelpers.delay(800) // Симуляция сохранения - - // Симуляция возможной ошибки (5% вероятность) - if (Math.random() < 0.05) { - throw new Error('Ошибка сети при сохранении') - } - - console.log('Сохранены условия:', value) - return { - id: Math.floor(Math.random() * 1000), - success: true, - message: 'Условия успешно сохранены', - } - } catch (err) { - const error = - err instanceof Error ? err : new Error('Неизвестная ошибка') - setIsError(true) - setError(error) - throw error - } finally { - setIsPending(false) - } }, [] ) - return { mutateAsync, isPending, isError, error } -} - -// Вспомогательная функция для валидации -const validateConditionValue = ( - value: any, - dataType: string, - minValue?: number, - maxValue?: number -): { isValid: boolean; error?: string } => { - if (dataType === 'number') { - const numValue = parseFloat(value) - if (isNaN(numValue)) { - return { isValid: false, error: 'Должно быть числом' } + const deleteConditions = useCallback(async (conditionId: string) => { + try { + setIsLoading(true) + setIsError(false) + await dailyConditionsApi.delete(conditionId) + } catch (error) { + setIsError(true) + console.error('Ошибка удаления условий:', error) + throw error + } finally { + setIsLoading(false) } - if (minValue !== undefined && numValue < minValue) { - return { isValid: false, error: `Минимальное значение: ${minValue}` } - } - if (maxValue !== undefined && numValue > maxValue) { - return { isValid: false, error: `Максимальное значение: ${maxValue}` } - } - } - return { isValid: true } -} - -// Вспомогательная функция для получения ключа условия -const getConditionKey = (conditionTypeName: string): string => { - return conditionTypeName - .toLowerCase() - .replace(/\s+/g, '_') - .replace(/[^\w]/g, '') -} - -// Основной хук для управления состоянием элемента условий поверки -export function useVerificationConditionsElement( - initialConfig: { - selectedLaboratoryId?: number - selectedDate?: string - autoSave?: boolean - } = {} -) { - const [selectedLaboratoryId, setSelectedLaboratoryId] = useState< - number | undefined - >(initialConfig.selectedLaboratoryId) - const [selectedDate, setSelectedDate] = useState( - initialConfig.selectedDate || new Date().toISOString().split('T')[0] - ) - const [localConditions, setLocalConditions] = useState>( - {} - ) - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) - const [validationErrors, setValidationErrors] = useState( - [] - ) - - // Загружаем данные - const laboratoriesQuery = useLaboratories() - const elementConditionsQuery = useElementConditions( - selectedLaboratoryId, - selectedDate - ) - const saveConditionsMutation = useSaveConditions() - - // Получаем выбранную лабораторию - const selectedLaboratory = laboratoriesQuery.data?.find( - lab => lab.id === selectedLaboratoryId - ) - - // Синхронизируем локальные условия с загруженными данными - useEffect(() => { - if (elementConditionsQuery.data && !hasUnsavedChanges) { - const conditions: Record = {} - elementConditionsQuery.data.conditions.forEach(cond => { - const key = getConditionKey(cond.typeName) - conditions[key] = cond.value - }) - setLocalConditions(conditions) - } - }, [elementConditionsQuery.data, hasUnsavedChanges]) - - // Валидация условий - const validateConditions = useCallback( - (conditions: Record): ValidationError[] => { - const errors: ValidationError[] = [] - - if (!elementConditionsQuery.data) return errors - - elementConditionsQuery.data.conditions.forEach(conditionDef => { - const key = getConditionKey(conditionDef.typeName) - const value = conditions[key] - - // Проверка обязательных полей - if ( - conditionDef.isRequired && - (value === undefined || value === null || value === '') - ) { - errors.push({ - field: key, - message: `${conditionDef.typeName} обязательно для заполнения`, - }) - return - } - - // Валидация значения - if (value !== undefined && value !== null && value !== '') { - const validation = validateConditionValue( - value, - 'number', - conditionDef.minValue, - conditionDef.maxValue - ) - - if (!validation.isValid) { - errors.push({ - field: key, - message: validation.error!, - value, - min: conditionDef.minValue, - max: conditionDef.maxValue, - }) - } - } - }) - - return errors - }, - [elementConditionsQuery.data] - ) - - // Обновление значения условия - const updateCondition = useCallback( - (key: string, value: any) => { - setLocalConditions(prev => ({ - ...prev, - [key]: value, - })) - setHasUnsavedChanges(true) - - // Валидируем новые условия - const newConditions = { ...localConditions, [key]: value } - const errors = validateConditions(newConditions) - setValidationErrors(errors) - }, - [localConditions, validateConditions] - ) - - // Смена лаборатории - const changeLaboratory = useCallback( - (laboratoryId: number) => { - if (hasUnsavedChanges) { - const confirmed = window.confirm( - 'У вас есть несохранённые изменения. Продолжить без сохранения?' - ) - if (!confirmed) return - } - - setSelectedLaboratoryId(laboratoryId) - setLocalConditions({}) - setHasUnsavedChanges(false) - setValidationErrors([]) - }, - [hasUnsavedChanges] - ) - - // Смена даты - const changeDate = useCallback( - (date: string) => { - if (hasUnsavedChanges) { - const confirmed = window.confirm( - 'У вас есть несохранённые изменения. Продолжить без сохранения?' - ) - if (!confirmed) return - } - - setSelectedDate(date) - setLocalConditions({}) - setHasUnsavedChanges(false) - setValidationErrors([]) - }, - [hasUnsavedChanges] - ) - - // Сохранение условий - const saveConditions = useCallback(async () => { - if (!selectedLaboratoryId || !selectedDate) { - throw new Error('Не выбрана лаборатория или дата') - } - - const errors = validateConditions(localConditions) - if (errors.length > 0) { - setValidationErrors(errors) - throw new Error('Есть ошибки валидации') - } - - const value: VerificationConditionsValue = { - laboratoryId: selectedLaboratoryId, - date: selectedDate, - conditions: localConditions, - } - - const result = await saveConditionsMutation.mutateAsync(value) - setHasUnsavedChanges(false) - setValidationErrors([]) - return result - }, [ - selectedLaboratoryId, - selectedDate, - localConditions, - validateConditions, - saveConditionsMutation, - ]) - - // Автосохранение - useEffect(() => { - if ( - initialConfig.autoSave && - hasUnsavedChanges && - validationErrors.length === 0 - ) { - const timeoutId = setTimeout(() => { - saveConditions().catch(console.error) - }, 2000) // Автосохранение через 2 секунды - - return () => clearTimeout(timeoutId) - } - }, [ - initialConfig.autoSave, - hasUnsavedChanges, - validationErrors.length, - saveConditions, - ]) - - // Получение текущего значения для компонента - const getCurrentValue = useCallback((): - | VerificationConditionsValue - | undefined => { - if (!selectedLaboratoryId || !selectedDate) return undefined - - return { - laboratoryId: selectedLaboratoryId, - date: selectedDate, - conditions: localConditions, - } - }, [selectedLaboratoryId, selectedDate, localConditions]) - - // Форматирование значения с единицей измерения - const formatValueWithUnit = useCallback( - (value: any, unit?: string): string => { - if (value === undefined || value === null || value === '') { - return '-' - } - return unit ? `${value} ${unit}` : value.toString() - }, - [] - ) + }, []) return { - // Данные - laboratories: laboratoriesQuery.data || [], - selectedLaboratory, - selectedLaboratoryId, - selectedDate, - conditions: elementConditionsQuery.data, - localConditions, - - // Состояние - isLoading: laboratoriesQuery.isLoading || elementConditionsQuery.isLoading, - isError: laboratoriesQuery.isError || elementConditionsQuery.isError, - isSaving: saveConditionsMutation.isPending, - hasUnsavedChanges, - validationErrors, - - // Действия - changeLaboratory, - changeDate, - updateCondition, - saveConditions, - getCurrentValue, - - // Вспомогательные функции - formatValueWithUnit, - getConditionKey, + isLoading, + isError, + loadConditions, + deleteConditions, } } diff --git a/src/component/VerificationConditionsElement/index.ts b/src/component/VerificationConditionsElement/index.ts index 44f4f66..672d1f4 100644 --- a/src/component/VerificationConditionsElement/index.ts +++ b/src/component/VerificationConditionsElement/index.ts @@ -1,4 +1,4 @@ -export { verificationConditionsDefinition } from './definition' +export { verificationConditionsElementDefinition as verificationConditionsDefinition } from './definition' export { VerificationConditionsEditor } from './VerificationConditionsEditor' export { VerificationConditionsPreview } from './VerificationConditionsPreview' export { VerificationConditionsRender } from './VerificationConditionsRender' diff --git a/src/component/VerificationConditionsElement/mockData.ts b/src/component/VerificationConditionsElement/mockData.ts index 0689585..c704e54 100644 --- a/src/component/VerificationConditionsElement/mockData.ts +++ b/src/component/VerificationConditionsElement/mockData.ts @@ -1,280 +1,2 @@ -import { ConditionType, Laboratory } from './types' - -// Моковые типы условий (из БД схемы) -export const mockConditionTypes: ConditionType[] = [ - { - id: 1, - name: 'Температура', - unit: '°C', - dataType: 'number', - description: 'Температура окружающей среды', - createdAt: '2024-01-01T00:00:00Z', - }, - { - id: 2, - name: 'Влажность', - unit: '%', - dataType: 'number', - description: 'Относительная влажность воздуха', - createdAt: '2024-01-01T00:00:00Z', - }, - { - id: 3, - name: 'Атмосферное давление', - unit: 'кПа', - dataType: 'number', - description: 'Атмосферное давление', - createdAt: '2024-01-01T00:00:00Z', - }, - { - id: 4, - name: 'Давление среды', - unit: 'МПа', - dataType: 'number', - description: 'Давление рабочей среды', - createdAt: '2024-01-01T00:00:00Z', - }, - { - id: 5, - name: 'Напряжение сети', - unit: 'В', - dataType: 'number', - description: 'Напряжение питающей сети', - createdAt: '2024-01-01T00:00:00Z', - }, - { - id: 6, - name: 'Частота сети', - unit: 'Гц', - dataType: 'number', - description: 'Частота питающей сети', - createdAt: '2024-01-01T00:00:00Z', - }, -] - -// Моковые лаборатории с полными данными условий -export const mockLaboratories: Laboratory[] = [ - { - id: 1, - name: 'Лаборатория температуры', - code: 'TEMP_LAB', - description: 'Лаборатория поверки температурных приборов', - address: 'Корпус А, комната 101', - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - conditionTypes: [ - { - id: 1, - laboratoryId: 1, - conditionTypeId: 1, - conditionType: mockConditionTypes[0], // Температура - isRequired: true, - defaultValue: '20', - minValue: 15, - maxValue: 30, - createdAt: '2024-01-01T00:00:00Z', - }, - { - id: 2, - laboratoryId: 1, - conditionTypeId: 2, - conditionType: mockConditionTypes[1], // Влажность - isRequired: true, - defaultValue: '50', - minValue: 30, - maxValue: 80, - createdAt: '2024-01-01T00:00:00Z', - }, - { - id: 3, - laboratoryId: 1, - conditionTypeId: 3, - conditionType: mockConditionTypes[2], // Атмосферное давление - isRequired: false, - defaultValue: '101.3', - minValue: 80, - maxValue: 120, - createdAt: '2024-01-01T00:00:00Z', - }, - ], - }, - { - id: 2, - name: 'Лаборатория давления', - code: 'PRESS_LAB', - description: 'Лаборатория поверки приборов давления', - address: 'Корпус Б, комната 205', - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - conditionTypes: [ - { - id: 4, - laboratoryId: 2, - conditionTypeId: 1, - conditionType: mockConditionTypes[0], // Температура - isRequired: true, - defaultValue: '23', - minValue: 18, - maxValue: 28, - createdAt: '2024-01-01T00:00:00Z', - }, - { - id: 5, - laboratoryId: 2, - conditionTypeId: 2, - conditionType: mockConditionTypes[1], // Влажность - isRequired: false, - defaultValue: '45', - minValue: 20, - maxValue: 70, - createdAt: '2024-01-01T00:00:00Z', - }, - { - id: 6, - laboratoryId: 2, - conditionTypeId: 3, - conditionType: mockConditionTypes[2], // Атмосферное давление - isRequired: true, - defaultValue: '101.3', - minValue: 90, - maxValue: 110, - createdAt: '2024-01-01T00:00:00Z', - }, - { - id: 7, - laboratoryId: 2, - conditionTypeId: 4, - conditionType: mockConditionTypes[3], // Давление среды - isRequired: true, - defaultValue: '0.1', - minValue: 0, - maxValue: 100, - createdAt: '2024-01-01T00:00:00Z', - }, - ], - }, - { - id: 3, - name: 'Электрическая лаборатория', - code: 'ELEC_LAB', - description: 'Лаборатория поверки электрических приборов', - address: 'Корпус В, комната 310', - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - conditionTypes: [ - { - id: 8, - laboratoryId: 3, - conditionTypeId: 1, - conditionType: mockConditionTypes[0], // Температура - isRequired: true, - defaultValue: '22', - minValue: 20, - maxValue: 25, - createdAt: '2024-01-01T00:00:00Z', - }, - { - id: 9, - laboratoryId: 3, - conditionTypeId: 2, - conditionType: mockConditionTypes[1], // Влажность - isRequired: true, - defaultValue: '60', - minValue: 40, - maxValue: 70, - createdAt: '2024-01-01T00:00:00Z', - }, - { - id: 10, - laboratoryId: 3, - conditionTypeId: 5, - conditionType: mockConditionTypes[4], // Напряжение сети - isRequired: true, - defaultValue: '220', - minValue: 198, - maxValue: 242, - createdAt: '2024-01-01T00:00:00Z', - }, - { - id: 11, - laboratoryId: 3, - conditionTypeId: 6, - conditionType: mockConditionTypes[5], // Частота сети - isRequired: true, - defaultValue: '50', - minValue: 49.5, - maxValue: 50.5, - createdAt: '2024-01-01T00:00:00Z', - }, - ], - }, -] - -// Функция для получения всех доступных условий (для выбора в элементе) -export const getAllAvailableConditions = () => { - return [ - { - key: 'laboratory', - name: 'Название лаборатории', - unit: '', - dataType: 'string' as const, - }, - { key: 'date', name: 'Дата', unit: '', dataType: 'string' as const }, - ...mockConditionTypes.map(type => ({ - key: type.name.toLowerCase().replace(/\s+/g, '_').replace(/[^\w]/g, ''), - name: type.name, - unit: type.unit || '', - dataType: type.dataType, - })), - ] -} - -// Упрощенный API helper -export const mockApiHelpers = { - // Получить все лаборатории - getLaboratories: async (): Promise => { - await new Promise(resolve => setTimeout(resolve, 300)) - return mockLaboratories - }, - - // Получить лабораторию по ID - getLaboratoryById: (id: number): Laboratory | undefined => { - return mockLaboratories.find(lab => lab.id === id) - }, - - // Получить все доступные условия - getAllConditions: () => getAllAvailableConditions(), - - // Получить условия конкретной лаборатории - getLaboratoryConditions: (laboratoryId: number) => { - const lab = mockLaboratories.find(l => l.id === laboratoryId) - if (!lab) return [] - - return [ - { - key: 'laboratory', - name: 'Название лаборатории', - unit: '', - dataType: 'string' as const, - }, - { key: 'date', name: 'Дата', unit: '', dataType: 'string' as const }, - ...(lab.conditionTypes?.map(ct => ({ - key: - ct.conditionType?.name - .toLowerCase() - .replace(/\s+/g, '_') - .replace(/[^\w]/g, '') || `condition_${ct.id}`, - name: ct.conditionType?.name || `Условие ${ct.id}`, - unit: ct.conditionType?.unit || '', - dataType: ct.conditionType?.dataType || ('string' as const), - isRequired: ct.isRequired, - defaultValue: ct.defaultValue, - minValue: ct.minValue, - maxValue: ct.maxValue, - })) || []), - ] - }, - - // Симуляция задержки API - delay: (ms: number = 500) => new Promise(resolve => setTimeout(resolve, ms)), -} +// Флаг для переключения между mock и реальными данными +export const USE_MOCK_DATA = false diff --git a/src/component/VerificationConditionsElement/types.ts b/src/component/VerificationConditionsElement/types.ts index 0a1b70a..eb2f71f 100644 --- a/src/component/VerificationConditionsElement/types.ts +++ b/src/component/VerificationConditionsElement/types.ts @@ -1,109 +1,101 @@ -// Типы для элемента "Условия поверки" +// Типы для элемента "Условия поверки" на основе новой API схемы -export interface Laboratory { - id: number - name: string - code: string - description?: string - address?: string - conditionTypes?: LaboratoryConditionType[] - createdAt: string - updatedAt: string -} - -export interface ConditionType { - id: number - name: string - unit?: string - dataType: 'number' | 'string' | 'boolean' - description?: string - createdAt: string -} - -export interface LaboratoryConditionType { - id: number - laboratoryId: number - conditionTypeId: number - conditionType?: ConditionType - isRequired: boolean - defaultValue?: string - minValue?: number - maxValue?: number - createdAt: string -} - -export interface DailyConditions { - id: number - laboratoryId: number - laboratory?: Laboratory - date: string // YYYY-MM-DD - conditions: Record // JSON объект с условиями - createdAt: string - updatedAt: string - createdBy?: string -} - -export interface ConditionValue { - typeId: number - typeName: string - unit?: string - value: any - isRequired: boolean - minValue?: number - maxValue?: number -} - -export interface ElementConditionsData { - laboratoryId: number - laboratoryName: string - date: string - conditions: ConditionValue[] - hasUnsavedChanges: boolean -} - -// Привязка условия к ячейке -export interface ConditionCellMapping { - cellIndex: number // Индекс целевой ячейки - conditionKey: string // Ключ условия ('laboratory', 'date', 'temperature', etc.) - conditionName: string // Название для отображения +// Маппинг условия в ячейку +export interface ConditionMapping { + conditionKey: string // ключ условия (temperature, humidity, etc.) + conditionName: string // название условия для отображения + cellIndex: number // индекс целевой ячейки } // Конфигурация элемента export interface VerificationConditionsConfig { - selectedLaboratoryId?: number // Выбранная лаборатория - targetCells: Array<{ - sheet: string - cell: string - }> // Целевые ячейки из основного редактора элементов - conditionMappings: ConditionCellMapping[] // Привязка условий к ячейкам + // Выбранная лаборатория (UUID) + selectedLaboratoryId?: string + // Выбранная дата + selectedDate?: string + // Маппинг условий в ячейки + conditionMappings?: ConditionMapping[] + // Показывать ли единицы измерения + showUnits?: boolean } // Значение элемента export interface VerificationConditionsValue { - laboratoryId: number - date: string - conditions: Record // Значения условий {temperature: 23.5, humidity: 45.2} + date: string // дата в формате YYYY-MM-DD + conditions: Record // условия поверки {temperature: "20", humidity: "50", ...} } -// Ответы API -export interface ApiResponse { - success: boolean - data?: T - message?: string - error?: string +// Тип условия из API +export interface ConditionType { + id: string + name: string + unit: string + created_at: string + updated_at: string + deleted_at: string | null } -export interface SaveConditionsResponse { - id: number - success: boolean - message: string +// Лаборатория из API +export interface Laboratory { + id: string + name: string + condition_types: ConditionType[] + created_at: string + updated_at: string + deleted_at: string | null } -// Ошибки валидации -export interface ValidationError { - field: string - message: string - value?: any - min?: number - max?: number +// Ежедневное условие из API +export interface DailyCondition { + id: string + laboratory_id: string + measurement_date: string + conditions: Record + created_at: string + updated_at: string + deleted_at: string | null +} + +// Типы для API запросов/ответов +export interface GetLaboratoriesOutput { + laboratories: Laboratory[] +} + +export interface GetLaboratoryOutput { + laboratory: Laboratory | null +} + +export interface CreateDailyConditionInput { + laboratory_id: string + measurement_date: string + conditions: Record +} + +export interface CreateDailyConditionOutput { + id: string +} + +export interface UpdateDailyConditionInput { + laboratory_id?: string | null + measurement_date?: string | null + conditions?: Record | null +} + +export interface UpdateDailyConditionOutput { + id: string +} + +export interface GetDailyConditionsOutput { + daily_conditions: DailyCondition[] +} + +export interface GetDailyConditionOutput { + daily_condition: DailyCondition | null +} + +// Параметры фильтрации для получения ежедневных условий +export interface DailyConditionsFilters { + laboratory_id?: string + start_date?: string + end_date?: string } diff --git a/src/component/ui/calendar.tsx b/src/component/ui/calendar.tsx new file mode 100644 index 0000000..eb9bdc9 --- /dev/null +++ b/src/component/ui/calendar.tsx @@ -0,0 +1,213 @@ +'use client' + +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from 'lucide-react' +import * as React from 'react' +import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker' + +import { Button, buttonVariants } from '@/component/ui/button' +import { cn } from '@/lib/utils' + +function Calendar({ + className, + classNames, + showOutsideDays = true, + captionLayout = 'label', + buttonVariant = 'ghost', + formatters, + components, + ...props +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps['variant'] +}) { + const defaultClassNames = getDefaultClassNames() + + return ( + svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: date => + date.toLocaleString('default', { month: 'short' }), + ...formatters, + }} + classNames={{ + root: cn('w-fit', defaultClassNames.root), + months: cn( + 'flex gap-4 flex-col md:flex-row relative', + defaultClassNames.months + ), + month: cn('flex flex-col w-full gap-4', defaultClassNames.month), + nav: cn( + 'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between', + defaultClassNames.nav + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none', + defaultClassNames.button_previous + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none', + defaultClassNames.button_next + ), + month_caption: cn( + 'flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)', + defaultClassNames.month_caption + ), + dropdowns: cn( + 'w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5', + defaultClassNames.dropdowns + ), + dropdown_root: cn( + 'relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md', + defaultClassNames.dropdown_root + ), + dropdown: cn( + 'absolute bg-popover inset-0 opacity-0', + defaultClassNames.dropdown + ), + caption_label: cn( + 'select-none font-medium', + captionLayout === 'label' + ? 'text-sm' + : 'rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5', + defaultClassNames.caption_label + ), + table: 'w-full border-collapse', + weekdays: cn('flex', defaultClassNames.weekdays), + weekday: cn( + 'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none', + defaultClassNames.weekday + ), + week: cn('flex w-full mt-2', defaultClassNames.week), + week_number_header: cn( + 'select-none w-(--cell-size)', + defaultClassNames.week_number_header + ), + week_number: cn( + 'text-[0.8rem] select-none text-muted-foreground', + defaultClassNames.week_number + ), + day: cn( + 'relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none', + defaultClassNames.day + ), + range_start: cn( + 'rounded-l-md bg-accent', + defaultClassNames.range_start + ), + range_middle: cn('rounded-none', defaultClassNames.range_middle), + range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end), + today: cn( + 'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none', + defaultClassNames.today + ), + outside: cn( + 'text-muted-foreground aria-selected:text-muted-foreground', + defaultClassNames.outside + ), + disabled: cn( + 'text-muted-foreground opacity-50', + defaultClassNames.disabled + ), + hidden: cn('invisible', defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className, rootRef, ...props }) => { + return ( +
+ ) + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === 'left') { + return ( + + ) + } + + if (orientation === 'right') { + return ( + + ) + } + + return ( + + ) + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ) + }, + ...components, + }} + {...props} + /> + ) +} + +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps) { + const defaultClassNames = getDefaultClassNames() + + const ref = React.useRef(null) + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus() + }, [modifiers.focused]) + + return ( + +
+ +
+
+ {Array.from({ length: skeletonCount }, (_, index) => ( + + ))} +
+
+
+
+ ) +} diff --git a/src/component/StandardsElement/StandardsSelector.tsx b/src/entitiy/element/model/implementations/StandardsElement/StandardsSelector.tsx similarity index 100% rename from src/component/StandardsElement/StandardsSelector.tsx rename to src/entitiy/element/model/implementations/StandardsElement/StandardsSelector.tsx diff --git a/src/component/StandardsElement/api.ts b/src/entitiy/element/model/implementations/StandardsElement/api.ts similarity index 100% rename from src/component/StandardsElement/api.ts rename to src/entitiy/element/model/implementations/StandardsElement/api.ts diff --git a/src/entitiy/element/model/implementations/StandardsElement/definition.tsx b/src/entitiy/element/model/implementations/StandardsElement/definition.tsx new file mode 100644 index 0000000..8a5c513 --- /dev/null +++ b/src/entitiy/element/model/implementations/StandardsElement/definition.tsx @@ -0,0 +1,188 @@ +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 { CellTarget } from '@/type/template' +import { FileText, Settings, Weight } from 'lucide-react' +import { useEffect, useState } from 'react' +import { useStandards } from './hooks' +import { StandardsEditor } from './StandardsEditor' +import { StandardsPreview } from './StandardsPreview' +import { StandardsSelector } from './StandardsSelector' + +export interface StandardsConfig { + maxItems: number + targetCells: CellTarget[] + selectedStandards?: string[] + name?: string +} + +// Новый интерфейс для значения элемента эталонов +export interface StandardsValue { + standardIds: string[] + standardNames: string[] // пронумерованные protocol_name для записи в ячейки (1. Name, 2. Name) +} + +export const standardsDefinition: ElementDefinition< + StandardsConfig, + StandardsValue +> = { + type: 'standards', + label: 'Выбор эталонов', + icon: , + version: 1, + defaultConfig: { + maxItems: 7, + targetCells: [], + selectedStandards: [], + name: '', + }, + Editor: StandardsEditor, + Preview: StandardsPreview, + Render: ({ config, value, onChange }) => { + const [isStandardsDialogOpen, setIsStandardsDialogOpen] = useState(false) + const [selectedStandardIds, setSelectedStandardIds] = useState( + // Инициализируем из value.standardIds, config.selectedStandards или пустого массива + value?.standardIds || config.selectedStandards || [] + ) + + // Получаем реальные данные эталонов + const { data: standards = [] } = useStandards() + + // Синхронизируем selectedStandardIds с value и config + useEffect(() => { + const standardIds = value?.standardIds || config.selectedStandards || [] + setSelectedStandardIds(standardIds) + }, [value?.standardIds, config.selectedStandards]) + + // Получаем данные выбранных эталонов из реального API + const selectedStandardsData = selectedStandardIds + .map(id => standards.find(s => s.id === id)) + .filter(Boolean) + + // Автоматическая инициализация onChange при загрузке данных эталонов + useEffect(() => { + if (standards.length > 0 && selectedStandardIds.length > 0) { + const standardNames = selectedStandardIds + .map((id, index) => { + const standard = standards.find(s => s.id === id) + return standard?.protocol_name + ? `${index + 1}. ${standard.protocol_name}` + : '' + }) + .filter(Boolean) + + onChange?.({ + standardIds: selectedStandardIds, + standardNames, + }) + } + }, [selectedStandardIds, standards, onChange]) + + return ( +
+ {/* Блок с измерительными эталонами */} +
+
+ + +
+ +
+ {selectedStandardsData.length > 0 ? ( +
+ {selectedStandardsData.map( + (standard, index) => + standard && ( +
+
+ + {index + 1} + +
+
+
+ {standard.name} +
+
+ + + {standard.registry_number} + +
+
+ + {standard.range} + +
+ ) + )} +
+ ) : ( +
+ + Эталоны не выбраны +
+ )} +
+
+ + setIsStandardsDialogOpen(false)} + value={selectedStandardIds} + onChange={standardIds => { + setSelectedStandardIds(standardIds) + + // Получаем protocol_name эталонов для выбранных ID с нумерацией + const standardNames = standardIds + .map((id, index) => { + const standard = standards.find(s => s.id === id) + return standard?.protocol_name + ? `${index + 1}. ${standard.protocol_name}` + : '' + }) + .filter(Boolean) + + // Сохраняем полную информацию: ID и protocol_name + onChange?.({ + standardIds, + standardNames, + }) + }} + maxItems={config.maxItems} + /> +
+ ) + }, + mapToCells: cfg => { + // Возвращаем ячейки из конфигурации + return cfg.targetCells || [] + }, + mapToCellValues: (cfg, value) => { + // Используем targetCells из конфигурации + const cells = cfg.targetCells || [] + + // value теперь содержит как ID, так и пронумерованные protocol_name эталонов + const standardNames = value?.standardNames || [] + + return cells.map((target, idx) => ({ + target, + value: standardNames[idx] || '', + })) + }, +} diff --git a/src/component/StandardsElement/hooks.ts b/src/entitiy/element/model/implementations/StandardsElement/hooks.ts similarity index 100% rename from src/component/StandardsElement/hooks.ts rename to src/entitiy/element/model/implementations/StandardsElement/hooks.ts diff --git a/src/component/StandardsElement/types.ts b/src/entitiy/element/model/implementations/StandardsElement/types.ts similarity index 100% rename from src/component/StandardsElement/types.ts rename to src/entitiy/element/model/implementations/StandardsElement/types.ts diff --git a/src/entitiy/element/model/interface.ts b/src/entitiy/element/model/interface.ts index 5d3af63..698edce 100644 --- a/src/entitiy/element/model/interface.ts +++ b/src/entitiy/element/model/interface.ts @@ -1,11 +1,10 @@ import { textDefinition } from '@/component/BasicElements' -import { calibrationConditionsDefinition } from '@/component/BasicElements/CalibrationConditionsElement' import { numberDefinition } from '@/component/BasicElements/NumberElement' import { selectDefinition } from '@/component/BasicElements/SelectElement' -import { standardsDefinition } from '@/component/StandardsElement/definition' -import { verificationConditionsDefinition } from '@/component/VerificationConditionsElement/definition' +import { verificationConditionsElementDefinition as verificationConditionsDefinition } from '@/component/VerificationConditionsElement/definition' import { buttonGroupDefinition } from '@/entitiy/element/model/implementations/ButtonGroup' import { dateDefinition } from '@/entitiy/element/model/implementations/DateElement' +import { standardsDefinition } from '@/entitiy/element/model/implementations/StandardsElement/definition' import { CellTarget } from '@/type/template' import { ReactNode } from 'react' @@ -67,7 +66,6 @@ export const elementDefinitions = { number: numberDefinition, date: dateDefinition, standards: standardsDefinition, - calibration_conditions: calibrationConditionsDefinition, verification_conditions: verificationConditionsDefinition, button_group: buttonGroupDefinition, } as const @@ -80,7 +78,6 @@ export function initializeElementRegistry() { registerElement('number', numberDefinition) registerElement('date', dateDefinition) registerElement('button_group', buttonGroupDefinition) - registerElement('calibration_conditions', calibrationConditionsDefinition) registerElement('standards', standardsDefinition) registerElement('verification_conditions', verificationConditionsDefinition) } diff --git a/src/page/StandardsManagementPage.tsx b/src/page/StandardsManagementPage.tsx index 8c493ee..36ec06c 100644 --- a/src/page/StandardsManagementPage.tsx +++ b/src/page/StandardsManagementPage.tsx @@ -1,14 +1,3 @@ -import { - useCreateStandard, - useDeleteStandard, - useStandards, - useUpdateStandard, -} from '@/component/StandardsElement/hooks' -import { - CreateStandardInput, - PatchStandardInput, - Standard, -} from '@/component/StandardsElement/types' import { Badge } from '@/component/ui/badge' import { Button } from '@/component/ui/button' import { @@ -19,6 +8,17 @@ import { } from '@/component/ui/dialog' import { Input } from '@/component/ui/input' import { Label } from '@/component/ui/label' +import { + useCreateStandard, + useDeleteStandard, + useStandards, + useUpdateStandard, +} from '@/entitiy/element/model/implementations/StandardsElement/hooks' +import { + CreateStandardInput, + PatchStandardInput, + Standard, +} from '@/entitiy/element/model/implementations/StandardsElement/types' import { Calendar, Edit, diff --git a/tailwind.config.js b/tailwind.config.js index 1bad2f1..950fea5 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,57 +1,57 @@ /** @type {import('tailwindcss').Config} */ export default { - darkMode: ['class'], - content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + darkMode: ['class'], + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], theme: { - extend: { - colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' - }, - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' - }, - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))' - } - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' - } - } + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + chart: { + 1: 'hsl(var(--chart-1))', + 2: 'hsl(var(--chart-2))', + 3: 'hsl(var(--chart-3))', + 4: 'hsl(var(--chart-4))', + 5: 'hsl(var(--chart-5))', + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + }, }, - plugins: [require("tailwindcss-animate")], + plugins: [require('tailwindcss-animate')], } diff --git a/yarn.lock b/yarn.lock index 048e048..9799791 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,11 @@ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.6.tgz#ec4070a04d76bae8ddbb10770ba55714a417b7c6" integrity sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q== +"@date-fns/tz@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@date-fns/tz/-/tz-1.2.0.tgz#81cb3211693830babaf3b96aff51607e143030a6" + integrity sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg== + "@dnd-kit/accessibility@^3.1.1": version "3.1.1" resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz#3b4202bd6bb370a0730f6734867785919beac6af" @@ -1639,6 +1644,16 @@ data-view-byte-offset@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" +date-fns-jalali@4.1.0-0: + version "4.1.0-0" + resolved "https://registry.yarnpkg.com/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz#9c7fb286004fab267a300d3e9f1ada9f10b4b6b0" + integrity sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg== + +date-fns@4.1.0, date-fns@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14" + integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg== + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -3207,6 +3222,15 @@ react-beautiful-dnd@^13.1.1: redux "^4.0.4" use-memo-one "^1.1.1" +react-day-picker@^9.8.0: + version "9.8.0" + resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-9.8.0.tgz#b16367fb26f3a967154e0e0f141297bd1f0a6e6b" + integrity sha512-E0yhhg7R+pdgbl/2toTb0xBhsEAtmAx1l7qjIWYfcxOy8w4rTSVfbtBoSzVVhPwKP/5E9iL38LivzoE3AQDhCQ== + dependencies: + "@date-fns/tz" "1.2.0" + date-fns "4.1.0" + date-fns-jalali "4.1.0-0" + react-dom@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"