diff --git a/database-schema.sql b/database-schema.sql new file mode 100644 index 0000000..b8c77ab --- /dev/null +++ b/database-schema.sql @@ -0,0 +1,90 @@ +-- Схема БД для элемента "Условия поверки" + +-- Таблица лабораторий +CREATE TABLE laboratories ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + code VARCHAR(50) UNIQUE NOT NULL, -- Уникальный код лаборатории + description TEXT, + address TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Таблица типов условий поверки (температура, влажность, давление и т.д.) +CREATE TABLE condition_types ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, -- "Температура", "Влажность", "Давление" + unit VARCHAR(20), -- "°C", "%", "кПа" + data_type VARCHAR(20) DEFAULT 'number', -- number, string, boolean + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Связь лаборатории с типами условий (какие условия отслеживает каждая лаборатория) +CREATE TABLE laboratory_condition_types ( + id SERIAL PRIMARY KEY, + laboratory_id INTEGER REFERENCES laboratories(id) ON DELETE CASCADE, + condition_type_id INTEGER REFERENCES condition_types(id) ON DELETE CASCADE, + is_required BOOLEAN DEFAULT false, -- Обязательно ли это условие для данной лаборатории + default_value VARCHAR(100), -- Значение по умолчанию + min_value DECIMAL(10,3), -- Минимальное значение (для валидации) + max_value DECIMAL(10,3), -- Максимальное значение (для валидации) + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(laboratory_id, condition_type_id) +); + +-- Таблица для сохранения условий по дням +CREATE TABLE daily_conditions ( + id SERIAL PRIMARY KEY, + laboratory_id INTEGER REFERENCES laboratories(id) ON DELETE CASCADE, + date DATE NOT NULL, + conditions JSONB NOT NULL, -- JSON объект с условиями: {"temperature": 23.5, "humidity": 45.2, "pressure": 101.3} + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(100), -- Кто создал запись + UNIQUE(laboratory_id, date) -- Одна запись условий на день для лаборатории +); + +-- Индексы для оптимизации +CREATE INDEX idx_daily_conditions_lab_date ON daily_conditions(laboratory_id, date); +CREATE INDEX idx_daily_conditions_date ON daily_conditions(date); +CREATE INDEX idx_laboratory_condition_types_lab ON laboratory_condition_types(laboratory_id); + +-- Вставка начальных данных + +-- Типы условий +INSERT INTO condition_types (name, unit, data_type, description) VALUES +('Температура', '°C', 'number', 'Температура окружающей среды'), +('Влажность', '%', 'number', 'Относительная влажность воздуха'), +('Атмосферное давление', 'кПа', 'number', 'Атмосферное давление'), +('Давление среды', 'МПа', 'number', 'Давление рабочей среды'), +('Напряжение сети', 'В', 'number', 'Напряжение питающей сети'), +('Частота сети', 'Гц', 'number', 'Частота питающей сети'); + +-- Примеры лабораторий +INSERT INTO laboratories (name, code, description) VALUES +('Лаборатория температуры', 'TEMP_LAB', 'Лаборатория поверки температурных приборов'), +('Лаборатория давления', 'PRESS_LAB', 'Лаборатория поверки приборов давления'), +('Электрическая лаборатория', 'ELEC_LAB', 'Лаборатория поверки электрических приборов'); + +-- Настройка условий для лабораторий +-- Лаборатория температуры +INSERT INTO laboratory_condition_types (laboratory_id, condition_type_id, is_required, default_value, min_value, max_value) VALUES +(1, 1, true, '20', 15, 30), -- Температура +(1, 2, true, '50', 30, 80), -- Влажность +(1, 3, false, '101.3', 80, 120); -- Атмосферное давление + +-- Лаборатория давления +INSERT INTO laboratory_condition_types (laboratory_id, condition_type_id, is_required, default_value, min_value, max_value) VALUES +(2, 1, true, '23', 18, 28), -- Температура +(2, 2, false, '45', 20, 70), -- Влажность +(2, 3, true, '101.3', 90, 110), -- Атмосферное давление +(2, 4, true, '0.1', 0, 100); -- Давление среды + +-- Электрическая лаборатория +INSERT INTO laboratory_condition_types (laboratory_id, condition_type_id, is_required, default_value, min_value, max_value) VALUES +(3, 1, true, '22', 20, 25), -- Температура +(3, 2, true, '60', 40, 70), -- Влажность +(3, 5, true, '220', 198, 242), -- Напряжение сети +(3, 6, true, '50', 49.5, 50.5); -- Частота сети \ No newline at end of file diff --git a/package.json b/package.json index e29f603..e96df00 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-context-menu": "^2.2.15", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", diff --git a/src/component/VerificationConditionsElement/VerificationConditionsEditor.tsx b/src/component/VerificationConditionsElement/VerificationConditionsEditor.tsx new file mode 100644 index 0000000..2c5a37c --- /dev/null +++ b/src/component/VerificationConditionsElement/VerificationConditionsEditor.tsx @@ -0,0 +1,227 @@ +import { Badge } from '@/component/ui/badge' +import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card' +import { Label } from '@/component/ui/label' +import { + Select, + SelectContent, + SelectItem, + 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' + +interface VerificationConditionsEditorProps { + config: VerificationConditionsConfig + onChange: (config: VerificationConditionsConfig) => void +} + +export function VerificationConditionsEditor({ + config, + onChange, +}: VerificationConditionsEditorProps) { + const { data: laboratories = [], isLoading } = useLaboratories() + + const updateConfig = (updates: Partial) => { + onChange({ ...config, ...updates }) + } + + // Получаем выбранную лабораторию + const selectedLab = laboratories.find( + lab => lab.id === config.selectedLaboratoryId + ) + + // Получаем все доступные условия (либо все из БД, либо условия конкретной лаборатории) + const availableConditions = config.selectedLaboratoryId + ? mockApiHelpers.getLaboratoryConditions(config.selectedLaboratoryId) + : mockApiHelpers.getAllConditions() + + // Обновление привязки условия к ячейке + const updateConditionMapping = (cellIndex: number, conditionKey: string) => { + const conditionName = + availableConditions.find(c => c.key === conditionKey)?.name || + conditionKey + + const newMappings = [...(config.conditionMappings || [])] + const existingIndex = newMappings.findIndex(m => m.cellIndex === cellIndex) + + 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) + } + } + + updateConfig({ conditionMappings: newMappings }) + } + + // Получить выбранное условие для ячейки + const getSelectedCondition = (cellIndex: number): string => { + const mapping = config.conditionMappings?.find( + m => m.cellIndex === cellIndex + ) + return mapping?.conditionKey || '__none__' + } + + return ( +
+ {/* Выбор лаборатории */} + + + + + Лаборатория + + + +
+ + +

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

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

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

+

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

+
+ +
+ {config.targetCells.map((cell, index) => ( +
+ + {cell.sheet}!{cell.cell} + + +
+ + +
+
+ ))} +
+ + {/* Подсказка с доступными условиями */} +
+

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

+
    + {availableConditions.map(condition => ( +
  • + • {condition.name} + {condition.unit && ` (${condition.unit})`} + {'isRequired' in condition && + condition.isRequired && + ' - обязательное'} +
  • + ))} +
+
+
+
+ )} + + {/* Подсказка если нет целевых ячеек */} + {config.targetCells.length === 0 && ( + + +
+ +

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

+

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

+
+
+
+ )} +
+ ) +} diff --git a/src/component/VerificationConditionsElement/VerificationConditionsPreview.tsx b/src/component/VerificationConditionsElement/VerificationConditionsPreview.tsx new file mode 100644 index 0000000..627b6c2 --- /dev/null +++ b/src/component/VerificationConditionsElement/VerificationConditionsPreview.tsx @@ -0,0 +1,131 @@ +import { Badge } from '@/component/ui/badge' +import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card' +import { Building2, Calendar, Droplets, Gauge, Thermometer } from 'lucide-react' +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: , + }, + ], +} + +export function VerificationConditionsPreview({ + config, +}: VerificationConditionsPreviewProps) { + const displayedConditions = mockData.conditions.slice(0, config.maxConditions) + + return ( + + + Условия поверки + + + {/* Селекторы лаборатории и даты */} +
+
+
+ + Лаборатория +
+
+ {config.selectedLaboratoryId + ? mockData.laboratory.name + : 'Не выбрана'} +
+
+ +
+
+ + Дата +
+
+ {config.selectedDate || mockData.date} +
+
+
+ + {/* Условия */} +
+
+ Условия +
+
+ {displayedConditions.map((condition, index) => ( +
+
+ {condition.icon} + {condition.typeName} +
+
+ {condition.value} + {config.showUnits && condition.unit && ( + + {condition.unit} + + )} +
+
+ ))} + + {mockData.conditions.length > config.maxConditions && ( +
+ ... и ещё {mockData.conditions.length - config.maxConditions}{' '} + условий +
+ )} +
+
+ + {/* Настройки */} +
+
+
+ Лист: {config.sheet} +
+
+ Ячейка: {config.startCell} +
+
Макс. условий: {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 new file mode 100644 index 0000000..b0b0699 --- /dev/null +++ b/src/component/VerificationConditionsElement/VerificationConditionsRender.tsx @@ -0,0 +1,311 @@ +import { Alert, AlertDescription } from '@/component/ui/alert' +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 { + AlertCircle, + Building2, + Calendar, + CheckCircle2, + Droplets, + Gauge, + Loader2, + Save, + Thermometer, + Zap, +} from 'lucide-react' +import { useEffect } from 'react' +import { useVerificationConditionsElement } from './hooks' +import { + VerificationConditionsConfig, + VerificationConditionsValue, +} from './types' + +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
+} + +export function VerificationConditionsRender({ + config, + value, + onChange, +}: VerificationConditionsRenderProps) { + const { + laboratories, + selectedLaboratory, + selectedLaboratoryId, + selectedDate, + conditions, + localConditions, + isLoading, + isError, + isSaving, + hasUnsavedChanges, + validationErrors, + changeLaboratory, + changeDate, + updateCondition, + saveConditions, + getCurrentValue, + formatValueWithUnit, + getConditionKey, + } = useVerificationConditionsElement({ + selectedLaboratoryId: config.selectedLaboratoryId || value?.laboratoryId, + selectedDate: config.selectedDate || value?.date, + autoSave: config.autoSave, + }) + + // Синхронизируем значение с родительским компонентом + 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) + } + } + }, [onChange, getCurrentValue, value]) + + // Обработка сохранения + const handleSave = async () => { + try { + await saveConditions() + } catch (error) { + console.error('Ошибка сохранения:', error) + } + } + + // Получение ошибки валидации для поля + const getFieldError = (fieldKey: string) => { + return validationErrors.find(error => error.field === fieldKey) + } + + if (isLoading) { + return ( + + + + Загрузка... + + + ) + } + + if (isError) { + return ( + + + + Ошибка загрузки данных условий поверки + + + ) + } + + return ( + + + + Условия поверки + {hasUnsavedChanges && !config.autoSave && ( + + )} + + + + {/* Селекторы лаборатории и даты */} +
+
+ + +
+ +
+ + 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) + + return ( +
+
+ {getConditionIcon(condition.typeName)} +
+
+ {condition.typeName} +
+ {config.showUnits && condition.unit && ( + + {condition.unit} + + )} +
+
+ +
+ updateCondition(key, e.target.value)} + placeholder={ + condition.minValue && condition.maxValue + ? `${condition.minValue}-${condition.maxValue}` + : 'Значение' + } + className={`w-20 text-right ${error ? 'border-destructive' : ''}`} + /> + {condition.isRequired && ( + * + )} +
+
+ ) + })} + + {conditions.conditions.length > config.maxConditions && ( +
+ ... и ещё{' '} + {conditions.conditions.length - config.maxConditions} условий +
+ )} +
+
+ )} + + {/* Ошибки валидации */} + {validationErrors.length > 0 && ( + + + +
+ {validationErrors.map((error, index) => ( +
+ {error.message} +
+ ))} +
+
+
+ )} + + {/* Статус */} +
+
+ {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 new file mode 100644 index 0000000..b10efe7 --- /dev/null +++ b/src/component/VerificationConditionsElement/api.ts @@ -0,0 +1,211 @@ +import { + ConditionType, + DailyConditions, + ElementConditionsData, + Laboratory, + SaveConditionsResponse, + VerificationConditionsValue, +} from './types' + +const BASE_URL = '/api/v1/verification-conditions' + +// API для лабораторий +export const laboratoriesApi = { + // Получить все лаборатории + getAll: async (): Promise => { + const response = await fetch(`${BASE_URL}/laboratories`) + if (!response.ok) { + throw new Error('Ошибка загрузки лабораторий') + } + return response.json() + }, + + // Получить лабораторию по ID + getById: async (id: number): Promise => { + const response = await fetch(`${BASE_URL}/laboratories/${id}`) + if (!response.ok) { + 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() + }, +} + +// API для ежедневных условий +export const dailyConditionsApi = { + // Получить условия по фильтрам + get: async (params: { + laboratoryId?: number + date?: string + dateFrom?: string + dateTo?: string + }): Promise => { + const searchParams = new URLSearchParams() + Object.entries(params).forEach(([key, value]) => { + if (value) searchParams.append(key, value.toString()) + }) + + const response = await fetch(`${BASE_URL}/daily-conditions?${searchParams}`) + if (!response.ok) { + throw new Error('Ошибка загрузки условий') + } + return response.json() + }, + + // Получить условия для элемента + getForElement: async ( + laboratoryId: number, + date: string + ): Promise => { + const response = await fetch( + `${BASE_URL}/daily-conditions/for-element?laboratoryId=${laboratoryId}&date=${date}` + ) + 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.ok) { + const error = await response.json() + throw new Error(error.message || 'Ошибка сохранения условий') + } + return response.json() + }, + + // Создать/обновить условия + save: async ( + data: Omit + ): Promise => { + const response = await fetch(`${BASE_URL}/daily-conditions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + const error = await response.json() + throw new Error(error.message || 'Ошибка сохранения условий') + } + return response.json() + }, + + // Обновить условия + update: async ( + id: number, + data: Partial + ): Promise => { + const response = await fetch(`${BASE_URL}/daily-conditions/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + throw new Error('Ошибка обновления условий') + } + return response.json() + }, + + // Удалить условия + delete: async (id: number): Promise => { + const response = await fetch(`${BASE_URL}/daily-conditions/${id}`, { + method: 'DELETE', + }) + if (!response.ok) { + throw new Error('Ошибка удаления условий') + } + }, +} + +// Вспомогательные функции +export const verificationConditionsUtils = { + // Валидация значения условия + 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: 'Должно быть числом' } + } + if (minValue !== undefined && numValue < minValue) { + return { isValid: false, error: `Минимальное значение: ${minValue}` } + } + if (maxValue !== undefined && numValue > maxValue) { + return { isValid: false, error: `Максимальное значение: ${maxValue}` } + } + } + return { isValid: true } + }, + + // Форматирование значения с единицей измерения + formatValueWithUnit: (value: any, unit?: string): string => { + if (value === undefined || value === null || value === '') { + return '-' + } + return unit ? `${value} ${unit}` : value.toString() + }, + + // Получение ключа условия для хранения в conditions JSON + getConditionKey: (conditionTypeName: string): string => { + return conditionTypeName + .toLowerCase() + .replace(/\s+/g, '_') + .replace(/[^\w]/g, '') + }, +} diff --git a/src/component/VerificationConditionsElement/definition.tsx b/src/component/VerificationConditionsElement/definition.tsx new file mode 100644 index 0000000..90b1f22 --- /dev/null +++ b/src/component/VerificationConditionsElement/definition.tsx @@ -0,0 +1,78 @@ +import { ElementDefinition } from '@/entitiy/element/model/interface' +import { CellTarget } from '@/type/template' +import { Gauge } from 'lucide-react' +import { + VerificationConditionsConfig, + VerificationConditionsValue, +} from './types' +import { VerificationConditionsEditor } from './VerificationConditionsEditor' +import { VerificationConditionsPreview } from './VerificationConditionsPreview' +import { VerificationConditionsRender } from './VerificationConditionsRender' + +export const verificationConditionsDefinition: ElementDefinition< + VerificationConditionsConfig, + VerificationConditionsValue +> = { + type: 'verification_conditions', + label: 'Условия поверки', + icon: , + version: 1, + + defaultConfig: { + targetCells: [], + conditionMappings: [], + }, + + Editor: VerificationConditionsEditor, + Preview: VerificationConditionsPreview, + Render: VerificationConditionsRender, + + mapToCells: config => { + // Возвращаем настроенные пользователем целевые ячейки + return config.targetCells.map(cell => ({ + sheet: cell.sheet, + cell: cell.cell, + })) + }, + + mapToCellValues: (config, value) => { + if (!value) 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 results + }, +} diff --git a/src/component/VerificationConditionsElement/hooks.ts b/src/component/VerificationConditionsElement/hooks.ts new file mode 100644 index 0000000..bcd2a86 --- /dev/null +++ b/src/component/VerificationConditionsElement/hooks.ts @@ -0,0 +1,431 @@ +import { useCallback, useEffect, useState } from 'react' +import { mockApiHelpers, mockLaboratories } from './mockData' +import { + ElementConditionsData, + Laboratory, + ValidationError, + VerificationConditionsValue, +} from './types' + +// Простой хук для загрузки лабораторий (используем моковые данные) +export function useLaboratories() { + const [data, setData] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [isError, setIsError] = useState(false) + + useEffect(() => { + const loadData = async () => { + try { + setIsLoading(true) + await mockApiHelpers.delay(300) // Симуляция загрузки + setData(mockLaboratories) + setIsError(false) + } catch (error) { + setIsError(true) + console.error('Ошибка загрузки лабораторий:', error) + } finally { + setIsLoading(false) + } + } + + loadData() + }, []) + + return { data, isLoading, isError } +} + +// Хук для загрузки конкретной лаборатории +export function useLaboratory(id: number | undefined) { + const [data, setData] = useState() + const [isLoading, setIsLoading] = useState(false) + const [isError, setIsError] = useState(false) + + useEffect(() => { + if (!id) { + setData(undefined) + return + } + + const loadData = async () => { + try { + setIsLoading(true) + await mockApiHelpers.delay(200) + const laboratory = mockApiHelpers.getLaboratoryById(id) + setData(laboratory) + setIsError(!laboratory) + } catch (error) { + setIsError(true) + console.error('Ошибка загрузки лаборатории:', error) + } finally { + setIsLoading(false) + } + } + + loadData() + }, [id]) + + return { data, isLoading, isError } +} + +// Хук для загрузки условий для элемента +export function useElementConditions( + laboratoryId: number | undefined, + date: string | undefined +) { + const [data, setData] = useState() + const [isLoading, setIsLoading] = useState(false) + const [isError, setIsError] = useState(false) + + useEffect(() => { + if (!laboratoryId || !date) { + setData(undefined) + return + } + + const loadData = async () => { + try { + setIsLoading(true) + await mockApiHelpers.delay(400) + const conditions = mockApiHelpers.getElementConditions( + laboratoryId, + date + ) + setData(conditions) + setIsError(!conditions) + } catch (error) { + setIsError(true) + console.error('Ошибка загрузки условий:', 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: 'Должно быть числом' } + } + 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, + } +} diff --git a/src/component/VerificationConditionsElement/index.ts b/src/component/VerificationConditionsElement/index.ts new file mode 100644 index 0000000..44f4f66 --- /dev/null +++ b/src/component/VerificationConditionsElement/index.ts @@ -0,0 +1,8 @@ +export { verificationConditionsDefinition } from './definition' +export { VerificationConditionsEditor } from './VerificationConditionsEditor' +export { VerificationConditionsPreview } from './VerificationConditionsPreview' +export { VerificationConditionsRender } from './VerificationConditionsRender' + +export * from './api' +export * from './hooks' +export * from './types' diff --git a/src/component/VerificationConditionsElement/mockData.ts b/src/component/VerificationConditionsElement/mockData.ts new file mode 100644 index 0000000..0689585 --- /dev/null +++ b/src/component/VerificationConditionsElement/mockData.ts @@ -0,0 +1,280 @@ +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)), +} diff --git a/src/component/VerificationConditionsElement/types.ts b/src/component/VerificationConditionsElement/types.ts new file mode 100644 index 0000000..0a1b70a --- /dev/null +++ b/src/component/VerificationConditionsElement/types.ts @@ -0,0 +1,109 @@ +// Типы для элемента "Условия поверки" + +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 VerificationConditionsConfig { + selectedLaboratoryId?: number // Выбранная лаборатория + targetCells: Array<{ + sheet: string + cell: string + }> // Целевые ячейки из основного редактора элементов + conditionMappings: ConditionCellMapping[] // Привязка условий к ячейкам +} + +// Значение элемента +export interface VerificationConditionsValue { + laboratoryId: number + date: string + conditions: Record // Значения условий {temperature: 23.5, humidity: 45.2} +} + +// Ответы API +export interface ApiResponse { + success: boolean + data?: T + message?: string + error?: string +} + +export interface SaveConditionsResponse { + id: number + success: boolean + message: string +} + +// Ошибки валидации +export interface ValidationError { + field: string + message: string + value?: any + min?: number + max?: number +} diff --git a/src/component/ui/alert.tsx b/src/component/ui/alert.tsx new file mode 100644 index 0000000..4564b6e --- /dev/null +++ b/src/component/ui/alert.tsx @@ -0,0 +1,62 @@ +import { cn } from '@/lib/utils' +import { cva, type VariantProps } from 'class-variance-authority' +import * as React from 'react' + +const alertVariants = cva( + 'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7', + { + variants: { + variant: { + default: 'bg-background text-foreground', + destructive: + 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', + warning: + 'border-amber-500/50 text-amber-700 bg-amber-50 dark:border-amber-500 dark:text-amber-400 dark:bg-amber-950 [&>svg]:text-amber-600 dark:[&>svg]:text-amber-400', + success: + 'border-green-500/50 text-green-700 bg-green-50 dark:border-green-500 dark:text-green-400 dark:bg-green-950 [&>svg]:text-green-600 dark:[&>svg]:text-green-400', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = 'Alert' + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = 'AlertTitle' + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = 'AlertDescription' + +export { Alert, AlertDescription, AlertTitle } diff --git a/src/entitiy/element/model/interface.ts b/src/entitiy/element/model/interface.ts index eabd4c7..5d3af63 100644 --- a/src/entitiy/element/model/interface.ts +++ b/src/entitiy/element/model/interface.ts @@ -3,6 +3,7 @@ import { calibrationConditionsDefinition } from '@/component/BasicElements/Calib 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 { buttonGroupDefinition } from '@/entitiy/element/model/implementations/ButtonGroup' import { dateDefinition } from '@/entitiy/element/model/implementations/DateElement' import { CellTarget } from '@/type/template' @@ -67,6 +68,7 @@ export const elementDefinitions = { date: dateDefinition, standards: standardsDefinition, calibration_conditions: calibrationConditionsDefinition, + verification_conditions: verificationConditionsDefinition, button_group: buttonGroupDefinition, } as const @@ -80,4 +82,5 @@ export function initializeElementRegistry() { registerElement('button_group', buttonGroupDefinition) registerElement('calibration_conditions', calibrationConditionsDefinition) registerElement('standards', standardsDefinition) + registerElement('verification_conditions', verificationConditionsDefinition) } diff --git a/src/hook/useMultiSheetEngine.ts b/src/hook/useMultiSheetEngine.ts index 2c26431..dd0857a 100644 --- a/src/hook/useMultiSheetEngine.ts +++ b/src/hook/useMultiSheetEngine.ts @@ -64,11 +64,45 @@ export function useMultiSheetEngine({ } }, [sheets]) + // Ссылка для отслеживания уже загруженных данных + const loadedDataRef = useRef('') + // Отдельный useEffect для перезагрузки данных при изменении initialData useEffect(() => { if (!workbookRef.current) return + // Предотвращаем случайную очистку данных при смене вкладки + if (document.hidden) { + if (process.env.NODE_ENV !== 'production') { + console.log( + '⚠️ useMultiSheetEngine: предотвращаем загрузку данных когда страница скрыта' + ) + } + return + } + + // Создаем ключ для проверки изменений данных + const dataKey = JSON.stringify(initialData) + + // Если данные уже загружены, не перезагружаем их + if ( + loadedDataRef.current === dataKey && + Object.keys(initialData).length > 0 + ) { + if (process.env.NODE_ENV !== 'production') { + console.log('⏭️ useMultiSheetEngine: данные уже загружены, пропускаем') + } + return + } + if (Object.keys(initialData).length > 0) { + if (process.env.NODE_ENV !== 'production') { + console.log( + '🔄 useMultiSheetEngine: загрузка initialData:', + Object.keys(initialData) + ) + } + // Очищаем только те листы, для которых есть новые данные for (const sheetName of Object.keys(initialData)) { const sheet = workbookRef.current.getSheet(sheetName) @@ -78,6 +112,7 @@ export function useMultiSheetEngine({ } workbookRef.current.fromJSON(initialData) + loadedDataRef.current = dataKey // Сохраняем ключ загруженных данных safeRecalc() } else { // Если данных нет, просто обновляем ревизию @@ -244,7 +279,15 @@ export function useMultiSheetEngine({ const setTemplateData = useCallback( (data: Record>) => { templateDataRef.current = JSON.parse(JSON.stringify(data)) // Глубокая копия - // console.log('📋 Установлены данные шаблона:', templateDataRef.current) + if (process.env.NODE_ENV !== 'production') { + console.log('📋 Установлены базовые данные шаблона:', { + sheets: Object.keys(templateDataRef.current), + cellCount: Object.values(templateDataRef.current).reduce( + (total, sheet) => total + Object.keys(sheet).length, + 0 + ), + }) + } }, [] ) @@ -293,14 +336,16 @@ export function useMultiSheetEngine({ if (normalizedCurrent !== normalizedTemplate) { // Если текущего значения нет (ячейка очищена), отправляем '' modifiedSheet[cellAddress] = currentVal - // console.log( - // `🔧 Пользовательское изменение ${sheetName}:${cellAddress}:`, - // { - // template: normalizedTemplate, - // current: normalizedCurrent, - // value: currentVal, - // } - // ) + if (process.env.NODE_ENV !== 'production') { + console.log( + `🔧 Пользовательское изменение ${sheetName}:${cellAddress}:`, + { + template: normalizedTemplate, + current: normalizedCurrent, + value: currentVal, + } + ) + } } }) @@ -309,6 +354,22 @@ export function useMultiSheetEngine({ } }) + if (process.env.NODE_ENV !== 'production') { + const totalModified = Object.values(modifiedData).reduce( + (total, sheet) => total + Object.keys(sheet).length, + 0 + ) + console.log(`📊 Найдено ${totalModified} измененных ячеек:`, { + sheets: Object.keys(modifiedData), + details: Object.fromEntries( + Object.entries(modifiedData).map(([sheetName, cells]) => [ + sheetName, + Object.keys(cells).length, + ]) + ), + }) + } + return modifiedData }, []) diff --git a/verification-conditions-api.md b/verification-conditions-api.md new file mode 100644 index 0000000..232de77 --- /dev/null +++ b/verification-conditions-api.md @@ -0,0 +1,332 @@ +# API для элемента "Условия поверки" + +## Базовый URL + +``` +/api/v1/verification-conditions +``` + +## Эндпоинты + +### 1. Лаборатории + +#### GET /laboratories + +Получить список всех лабораторий + +```json +Response 200: +[ + { + "id": 1, + "name": "Лаборатория температуры", + "code": "TEMP_LAB", + "description": "Лаборатория поверки температурных приборов", + "address": "ул. Примерная, 1", + "conditionTypes": [ + { + "id": 1, + "name": "Температура", + "unit": "°C", + "isRequired": true, + "defaultValue": "20", + "minValue": 15, + "maxValue": 30 + } + ] + } +] +``` + +#### GET /laboratories/:id + +Получить лабораторию по ID с настройками условий + +#### POST /laboratories + +Создать новую лабораторию + +```json +Request: +{ + "name": "Новая лаборатория", + "code": "NEW_LAB", + "description": "Описание", + "address": "Адрес" +} +``` + +#### PUT /laboratories/:id + +Обновить лабораторию + +#### DELETE /laboratories/:id + +Удалить лабораторию + +### 2. Типы условий + +#### GET /condition-types + +Получить все доступные типы условий + +```json +Response 200: +[ + { + "id": 1, + "name": "Температура", + "unit": "°C", + "dataType": "number", + "description": "Температура окружающей среды" + } +] +``` + +#### POST /condition-types + +Создать новый тип условия + +```json +Request: +{ + "name": "Новое условие", + "unit": "единица", + "dataType": "number", + "description": "Описание" +} +``` + +### 3. Настройка условий для лабораторий + +#### GET /laboratories/:labId/condition-types + +Получить настроенные типы условий для лаборатории + +#### POST /laboratories/:labId/condition-types + +Добавить тип условия к лаборатории + +```json +Request: +{ + "conditionTypeId": 1, + "isRequired": true, + "defaultValue": "20", + "minValue": 15, + "maxValue": 30 +} +``` + +#### PUT /laboratories/:labId/condition-types/:typeId + +Обновить настройки типа условия для лаборатории + +#### DELETE /laboratories/:labId/condition-types/:typeId + +Удалить тип условия из лаборатории + +### 4. Условия по дням (основная функциональность) + +#### GET /daily-conditions + +Получить условия по дням с фильтрацией + +``` +Query параметры: +- laboratoryId: ID лаборатории +- dateFrom: дата начала (YYYY-MM-DD) +- dateTo: дата окончания (YYYY-MM-DD) +- date: конкретная дата (YYYY-MM-DD) + +Примеры: +GET /daily-conditions?laboratoryId=1&dateFrom=2024-01-01&dateTo=2024-01-31 +GET /daily-conditions?laboratoryId=1&date=2024-01-15 +``` + +```json +Response 200: +[ + { + "id": 1, + "laboratoryId": 1, + "date": "2024-01-15", + "conditions": { + "temperature": 23.5, + "humidity": 45.2, + "atmosphericPressure": 101.3 + }, + "createdAt": "2024-01-15T10:30:00Z", + "updatedAt": "2024-01-15T10:30:00Z", + "createdBy": "user@example.com" + } +] +``` + +#### GET /daily-conditions/:id + +Получить условия для конкретного дня по ID + +#### POST /daily-conditions + +Создать/обновить условия для дня + +```json +Request: +{ + "laboratoryId": 1, + "date": "2024-01-15", + "conditions": { + "temperature": 23.5, + "humidity": 45.2, + "atmosphericPressure": 101.3 + } +} +``` + +#### PUT /daily-conditions/:id + +Обновить условия для дня + +#### DELETE /daily-conditions/:id + +Удалить запись условий + +### 5. Специальные эндпоинты для элемента + +#### GET /daily-conditions/for-element + +Получить условия для использования в элементе формы + +``` +Query параметры: +- laboratoryId: ID лаборатории (обязательный) +- date: дата (обязательный, YYYY-MM-DD) +``` + +```json +Response 200: +{ + "laboratoryId": 1, + "laboratoryName": "Лаборатория температуры", + "date": "2024-01-15", + "conditions": [ + { + "typeId": 1, + "typeName": "Температура", + "unit": "°C", + "value": 23.5, + "isRequired": true, + "minValue": 15, + "maxValue": 30 + }, + { + "typeId": 2, + "typeName": "Влажность", + "unit": "%", + "value": 45.2, + "isRequired": true, + "minValue": 30, + "maxValue": 80 + } + ], + "hasUnsavedChanges": false +} +``` + +#### POST /daily-conditions/save-for-element + +Сохранить условия из элемента формы + +```json +Request: +{ + "laboratoryId": 1, + "date": "2024-01-15", + "conditions": { + "temperature": 23.5, + "humidity": 45.2, + "atmosphericPressure": 101.3 + } +} + +Response 200: +{ + "id": 1, + "success": true, + "message": "Условия сохранены" +} +``` + +## Типы данных TypeScript + +```typescript +interface Laboratory { + id: number + name: string + code: string + description?: string + address?: string + conditionTypes?: LaboratoryConditionType[] + createdAt: string + updatedAt: string +} + +interface ConditionType { + id: number + name: string + unit?: string + dataType: 'number' | 'string' | 'boolean' + description?: string + createdAt: string +} + +interface LaboratoryConditionType { + id: number + laboratoryId: number + conditionTypeId: number + conditionType?: ConditionType + isRequired: boolean + defaultValue?: string + minValue?: number + maxValue?: number + createdAt: string +} + +interface DailyConditions { + id: number + laboratoryId: number + laboratory?: Laboratory + date: string // YYYY-MM-DD + conditions: Record // JSON объект + createdAt: string + updatedAt: string + createdBy?: string +} + +interface ConditionValue { + typeId: number + typeName: string + unit?: string + value: any + isRequired: boolean + minValue?: number + maxValue?: number +} + +interface ElementConditionsData { + laboratoryId: number + laboratoryName: string + date: string + conditions: ConditionValue[] + hasUnsavedChanges: boolean +} +``` + +## Коды ошибок + +- `400` - Неверные параметры запроса +- `404` - Лаборатория/условия не найдены +- `409` - Конфликт (условия для этой даты уже существуют) +- `422` - Ошибка валидации (значения не в допустимом диапазоне) +- `500` - Внутренняя ошибка сервера diff --git a/yarn.lock b/yarn.lock index b3a3330..048e048 100644 --- a/yarn.lock +++ b/yarn.lock @@ -379,6 +379,18 @@ resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30" integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== +"@radix-ui/react-context-menu@^2.2.15": + version "2.2.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.15.tgz#d61a7c8badbcad72fadb9ed87efaa21df60b0a99" + integrity sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-menu" "2.1.15" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.2.2" + "@radix-ui/react-context@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36"