баг при смене страницы
This commit is contained in:
90
database-schema.sql
Normal file
90
database-schema.sql
Normal file
@@ -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); -- Частота сети
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@radix-ui/react-checkbox": "^1.3.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-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
|||||||
@@ -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<VerificationConditionsConfig>) => {
|
||||||
|
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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Выбор лаборатории */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Building2 className="h-4 w-4" />
|
||||||
|
Лаборатория
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="laboratory">Выберите лабораторию</Label>
|
||||||
|
<Select
|
||||||
|
value={config.selectedLaboratoryId?.toString() || '__none__'}
|
||||||
|
onValueChange={value => {
|
||||||
|
const laboratoryId =
|
||||||
|
value === '__none__' ? undefined : parseInt(value)
|
||||||
|
updateConfig({
|
||||||
|
selectedLaboratoryId: laboratoryId,
|
||||||
|
conditionMappings: [], // Сбрасываем привязки при смене лаборатории
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Выберите лабораторию" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">Все условия из БД</SelectItem>
|
||||||
|
{laboratories.map(lab => (
|
||||||
|
<SelectItem key={lab.id} value={lab.id.toString()}>
|
||||||
|
{lab.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{config.selectedLaboratoryId
|
||||||
|
? 'Доступны только условия выбранной лаборатории'
|
||||||
|
: 'Доступны все условия из базы данных'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Привязка условий к ячейкам */}
|
||||||
|
{config.targetCells.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Target className="h-4 w-4" />
|
||||||
|
Привязка условий к ячейкам
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="rounded-md bg-blue-50 p-3 text-sm text-blue-800 dark:bg-blue-950 dark:text-blue-200">
|
||||||
|
<p className="font-medium">Настройка записи данных</p>
|
||||||
|
<p className="mt-1">
|
||||||
|
Выберите какое условие будет записываться в каждую целевую
|
||||||
|
ячейку.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{config.targetCells.map((cell, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center gap-3 rounded-md border p-3"
|
||||||
|
>
|
||||||
|
<Badge variant="outline" className="shrink-0">
|
||||||
|
{cell.sheet}!{cell.cell}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<Label className="text-xs text-muted-foreground">
|
||||||
|
Ячейка {index + 1} - выберите условие:
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={getSelectedCondition(index)}
|
||||||
|
onValueChange={value =>
|
||||||
|
updateConditionMapping(index, value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue placeholder="Выберите условие" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">Не выбрано</SelectItem>
|
||||||
|
{availableConditions.map(condition => (
|
||||||
|
<SelectItem key={condition.key} value={condition.key}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>{condition.name}</span>
|
||||||
|
{condition.unit && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{condition.unit}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Подсказка с доступными условиями */}
|
||||||
|
<div className="rounded-md bg-muted/50 p-3 text-sm text-muted-foreground">
|
||||||
|
<p className="font-medium">
|
||||||
|
Доступные условия{' '}
|
||||||
|
{selectedLab ? `для ${selectedLab.name}` : 'из базы данных'}:
|
||||||
|
</p>
|
||||||
|
<ul className="mt-1 space-y-1">
|
||||||
|
{availableConditions.map(condition => (
|
||||||
|
<li key={condition.key}>
|
||||||
|
• <strong>{condition.name}</strong>
|
||||||
|
{condition.unit && ` (${condition.unit})`}
|
||||||
|
{'isRequired' in condition &&
|
||||||
|
condition.isRequired &&
|
||||||
|
' - обязательное'}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Подсказка если нет целевых ячеек */}
|
||||||
|
{config.targetCells.length === 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="text-center text-sm text-muted-foreground">
|
||||||
|
<Target className="mx-auto mb-2 h-8 w-8 opacity-50" />
|
||||||
|
<p className="font-medium">Целевые ячейки не настроены</p>
|
||||||
|
<p className="mt-1">
|
||||||
|
Сначала настройте целевые ячейки в основном редакторе элементов,
|
||||||
|
затем вернитесь сюда для привязки условий.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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: <Thermometer className="h-3 w-3" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeName: 'Влажность',
|
||||||
|
value: 45.2,
|
||||||
|
unit: '%',
|
||||||
|
icon: <Droplets className="h-3 w-3" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeName: 'Атмосферное давление',
|
||||||
|
value: 101.3,
|
||||||
|
unit: 'кПа',
|
||||||
|
icon: <Gauge className="h-3 w-3" />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VerificationConditionsPreview({
|
||||||
|
config,
|
||||||
|
}: VerificationConditionsPreviewProps) {
|
||||||
|
const displayedConditions = mockData.conditions.slice(0, config.maxConditions)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-md">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm">Условия поверки</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Селекторы лаборатории и даты */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
||||||
|
<Building2 className="h-3 w-3" />
|
||||||
|
Лаборатория
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border bg-muted/30 p-2 text-sm">
|
||||||
|
{config.selectedLaboratoryId
|
||||||
|
? mockData.laboratory.name
|
||||||
|
: 'Не выбрана'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
Дата
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border bg-muted/30 p-2 text-sm">
|
||||||
|
{config.selectedDate || mockData.date}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Условия */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 text-xs font-medium text-muted-foreground">
|
||||||
|
Условия
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{displayedConditions.map((condition, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between rounded-md border bg-background p-2 text-sm"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{condition.icon}
|
||||||
|
<span className="font-medium">{condition.typeName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>{condition.value}</span>
|
||||||
|
{config.showUnits && condition.unit && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{condition.unit}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{mockData.conditions.length > config.maxConditions && (
|
||||||
|
<div className="text-center text-xs text-muted-foreground">
|
||||||
|
... и ещё {mockData.conditions.length - config.maxConditions}{' '}
|
||||||
|
условий
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Настройки */}
|
||||||
|
<div className="rounded-md bg-muted/50 p-2 text-xs text-muted-foreground">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div>
|
||||||
|
Лист: <span className="font-mono">{config.sheet}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Ячейка: <span className="font-mono">{config.startCell}</span>
|
||||||
|
</div>
|
||||||
|
<div>Макс. условий: {config.maxConditions}</div>
|
||||||
|
{config.autoSave && <div>✓ Автосохранение</div>}
|
||||||
|
{config.showUnits && <div>✓ Показывать единицы</div>}
|
||||||
|
{config.targetCells.length > 0 && (
|
||||||
|
<div>Ячеек настроено: {config.targetCells.length}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 <Thermometer className="h-3 w-3" />
|
||||||
|
if (name.includes('влажн')) return <Droplets className="h-3 w-3" />
|
||||||
|
if (name.includes('давлен')) return <Gauge className="h-3 w-3" />
|
||||||
|
if (name.includes('напряжен') || name.includes('частот'))
|
||||||
|
return <Zap className="h-3 w-3" />
|
||||||
|
return <div className="h-3 w-3 rounded-full bg-muted" />
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex items-center justify-center p-6">
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Загрузка...
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
Ошибка загрузки данных условий поверки
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="flex items-center justify-between text-sm">
|
||||||
|
<span>Условия поверки</span>
|
||||||
|
{hasUnsavedChanges && !config.autoSave && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isSaving || validationErrors.length > 0}
|
||||||
|
className="h-7"
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="mr-1 h-3 w-3" />
|
||||||
|
)}
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Селекторы лаборатории и даты */}
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<Label className="flex items-center gap-2 text-xs font-medium">
|
||||||
|
<Building2 className="h-3 w-3" />
|
||||||
|
Лаборатория
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedLaboratoryId?.toString() || ''}
|
||||||
|
onValueChange={value => changeLaboratory(parseInt(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue placeholder="Выберите лабораторию" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{laboratories.map(lab => (
|
||||||
|
<SelectItem key={lab.id} value={lab.id.toString()}>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{lab.name}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{lab.code}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="flex items-center gap-2 text-xs font-medium">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
Дата
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={selectedDate || ''}
|
||||||
|
onChange={e => changeDate(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Условия */}
|
||||||
|
{conditions && selectedLaboratoryId && selectedDate && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs font-medium text-muted-foreground">
|
||||||
|
Условия поверки
|
||||||
|
</Label>
|
||||||
|
<div className="mt-2 space-y-2">
|
||||||
|
{conditions.conditions
|
||||||
|
.slice(0, config.maxConditions)
|
||||||
|
.map((condition, index) => {
|
||||||
|
const key = getConditionKey(condition.typeName)
|
||||||
|
const error = getFieldError(key)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={condition.typeId}
|
||||||
|
className="flex items-center gap-3 rounded-md border bg-background p-3"
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||||
|
{getConditionIcon(condition.typeName)}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate text-sm font-medium">
|
||||||
|
{condition.typeName}
|
||||||
|
</div>
|
||||||
|
{config.showUnits && condition.unit && (
|
||||||
|
<Badge variant="secondary" className="mt-1 text-xs">
|
||||||
|
{condition.unit}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
value={localConditions[key] || ''}
|
||||||
|
onChange={e => updateCondition(key, e.target.value)}
|
||||||
|
placeholder={
|
||||||
|
condition.minValue && condition.maxValue
|
||||||
|
? `${condition.minValue}-${condition.maxValue}`
|
||||||
|
: 'Значение'
|
||||||
|
}
|
||||||
|
className={`w-20 text-right ${error ? 'border-destructive' : ''}`}
|
||||||
|
/>
|
||||||
|
{condition.isRequired && (
|
||||||
|
<span className="text-sm text-destructive">*</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{conditions.conditions.length > config.maxConditions && (
|
||||||
|
<div className="text-center text-xs text-muted-foreground">
|
||||||
|
... и ещё{' '}
|
||||||
|
{conditions.conditions.length - config.maxConditions} условий
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Ошибки валидации */}
|
||||||
|
{validationErrors.length > 0 && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{validationErrors.map((error, index) => (
|
||||||
|
<div key={index} className="text-sm">
|
||||||
|
{error.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Статус */}
|
||||||
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{config.autoSave && hasUnsavedChanges && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
Автосохранение...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{config.autoSave &&
|
||||||
|
!hasUnsavedChanges &&
|
||||||
|
selectedLaboratoryId &&
|
||||||
|
selectedDate && (
|
||||||
|
<div className="flex items-center gap-1 text-green-600">
|
||||||
|
<CheckCircle2 className="h-3 w-3" />
|
||||||
|
Сохранено
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedLaboratory && <div>{selectedLaboratory.name}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Подсказка для пустого состояния */}
|
||||||
|
{!selectedLaboratoryId && (
|
||||||
|
<div className="rounded-md border border-dashed bg-muted/30 p-4 text-center text-sm text-muted-foreground">
|
||||||
|
<Building2 className="mx-auto mb-2 h-6 w-6 opacity-50" />
|
||||||
|
<p>Выберите лабораторию для настройки условий поверки</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
211
src/component/VerificationConditionsElement/api.ts
Normal file
211
src/component/VerificationConditionsElement/api.ts
Normal file
@@ -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<Laboratory[]> => {
|
||||||
|
const response = await fetch(`${BASE_URL}/laboratories`)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Ошибка загрузки лабораторий')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
|
||||||
|
// Получить лабораторию по ID
|
||||||
|
getById: async (id: number): Promise<Laboratory> => {
|
||||||
|
const response = await fetch(`${BASE_URL}/laboratories/${id}`)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Лаборатория не найдена')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
|
||||||
|
// Создать лабораторию
|
||||||
|
create: async (
|
||||||
|
data: Omit<Laboratory, 'id' | 'createdAt' | 'updatedAt'>
|
||||||
|
): Promise<Laboratory> => {
|
||||||
|
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<ConditionType[]> => {
|
||||||
|
const response = await fetch(`${BASE_URL}/condition-types`)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Ошибка загрузки типов условий')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
|
|
||||||
|
// Создать тип условия
|
||||||
|
create: async (
|
||||||
|
data: Omit<ConditionType, 'id' | 'createdAt'>
|
||||||
|
): Promise<ConditionType> => {
|
||||||
|
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<DailyConditions[]> => {
|
||||||
|
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<ElementConditionsData> => {
|
||||||
|
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<SaveConditionsResponse> => {
|
||||||
|
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<DailyConditions, 'id' | 'createdAt' | 'updatedAt'>
|
||||||
|
): Promise<DailyConditions> => {
|
||||||
|
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<DailyConditions>
|
||||||
|
): Promise<DailyConditions> => {
|
||||||
|
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<void> => {
|
||||||
|
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, '')
|
||||||
|
},
|
||||||
|
}
|
||||||
78
src/component/VerificationConditionsElement/definition.tsx
Normal file
78
src/component/VerificationConditionsElement/definition.tsx
Normal file
@@ -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: <Gauge className="h-4 w-4" />,
|
||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
||||||
431
src/component/VerificationConditionsElement/hooks.ts
Normal file
431
src/component/VerificationConditionsElement/hooks.ts
Normal file
@@ -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<Laboratory[]>([])
|
||||||
|
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<Laboratory | undefined>()
|
||||||
|
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<ElementConditionsData | undefined>()
|
||||||
|
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<Error | null>(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<string | undefined>(
|
||||||
|
initialConfig.selectedDate || new Date().toISOString().split('T')[0]
|
||||||
|
)
|
||||||
|
const [localConditions, setLocalConditions] = useState<Record<string, any>>(
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
|
||||||
|
const [validationErrors, setValidationErrors] = useState<ValidationError[]>(
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Загружаем данные
|
||||||
|
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<string, any> = {}
|
||||||
|
elementConditionsQuery.data.conditions.forEach(cond => {
|
||||||
|
const key = getConditionKey(cond.typeName)
|
||||||
|
conditions[key] = cond.value
|
||||||
|
})
|
||||||
|
setLocalConditions(conditions)
|
||||||
|
}
|
||||||
|
}, [elementConditionsQuery.data, hasUnsavedChanges])
|
||||||
|
|
||||||
|
// Валидация условий
|
||||||
|
const validateConditions = useCallback(
|
||||||
|
(conditions: Record<string, any>): 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/component/VerificationConditionsElement/index.ts
Normal file
8
src/component/VerificationConditionsElement/index.ts
Normal file
@@ -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'
|
||||||
280
src/component/VerificationConditionsElement/mockData.ts
Normal file
280
src/component/VerificationConditionsElement/mockData.ts
Normal file
@@ -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<Laboratory[]> => {
|
||||||
|
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)),
|
||||||
|
}
|
||||||
109
src/component/VerificationConditionsElement/types.ts
Normal file
109
src/component/VerificationConditionsElement/types.ts
Normal file
@@ -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<string, any> // 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<string, any> // Значения условий {temperature: 23.5, humidity: 45.2}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ответы API
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
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
|
||||||
|
}
|
||||||
62
src/component/ui/alert.tsx
Normal file
62
src/component/ui/alert.tsx
Normal file
@@ -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<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Alert.displayName = 'Alert'
|
||||||
|
|
||||||
|
const AlertTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h5
|
||||||
|
ref={ref}
|
||||||
|
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertTitle.displayName = 'AlertTitle'
|
||||||
|
|
||||||
|
const AlertDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn('text-sm [&_p]:leading-relaxed', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDescription.displayName = 'AlertDescription'
|
||||||
|
|
||||||
|
export { Alert, AlertDescription, AlertTitle }
|
||||||
@@ -3,6 +3,7 @@ import { calibrationConditionsDefinition } from '@/component/BasicElements/Calib
|
|||||||
import { numberDefinition } from '@/component/BasicElements/NumberElement'
|
import { numberDefinition } from '@/component/BasicElements/NumberElement'
|
||||||
import { selectDefinition } from '@/component/BasicElements/SelectElement'
|
import { selectDefinition } from '@/component/BasicElements/SelectElement'
|
||||||
import { standardsDefinition } from '@/component/StandardsElement/definition'
|
import { standardsDefinition } from '@/component/StandardsElement/definition'
|
||||||
|
import { verificationConditionsDefinition } from '@/component/VerificationConditionsElement/definition'
|
||||||
import { buttonGroupDefinition } from '@/entitiy/element/model/implementations/ButtonGroup'
|
import { buttonGroupDefinition } from '@/entitiy/element/model/implementations/ButtonGroup'
|
||||||
import { dateDefinition } from '@/entitiy/element/model/implementations/DateElement'
|
import { dateDefinition } from '@/entitiy/element/model/implementations/DateElement'
|
||||||
import { CellTarget } from '@/type/template'
|
import { CellTarget } from '@/type/template'
|
||||||
@@ -67,6 +68,7 @@ export const elementDefinitions = {
|
|||||||
date: dateDefinition,
|
date: dateDefinition,
|
||||||
standards: standardsDefinition,
|
standards: standardsDefinition,
|
||||||
calibration_conditions: calibrationConditionsDefinition,
|
calibration_conditions: calibrationConditionsDefinition,
|
||||||
|
verification_conditions: verificationConditionsDefinition,
|
||||||
button_group: buttonGroupDefinition,
|
button_group: buttonGroupDefinition,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@@ -80,4 +82,5 @@ export function initializeElementRegistry() {
|
|||||||
registerElement('button_group', buttonGroupDefinition)
|
registerElement('button_group', buttonGroupDefinition)
|
||||||
registerElement('calibration_conditions', calibrationConditionsDefinition)
|
registerElement('calibration_conditions', calibrationConditionsDefinition)
|
||||||
registerElement('standards', standardsDefinition)
|
registerElement('standards', standardsDefinition)
|
||||||
|
registerElement('verification_conditions', verificationConditionsDefinition)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,11 +64,45 @@ export function useMultiSheetEngine({
|
|||||||
}
|
}
|
||||||
}, [sheets])
|
}, [sheets])
|
||||||
|
|
||||||
|
// Ссылка для отслеживания уже загруженных данных
|
||||||
|
const loadedDataRef = useRef<string>('')
|
||||||
|
|
||||||
// Отдельный useEffect для перезагрузки данных при изменении initialData
|
// Отдельный useEffect для перезагрузки данных при изменении initialData
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workbookRef.current) return
|
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 (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)) {
|
for (const sheetName of Object.keys(initialData)) {
|
||||||
const sheet = workbookRef.current.getSheet(sheetName)
|
const sheet = workbookRef.current.getSheet(sheetName)
|
||||||
@@ -78,6 +112,7 @@ export function useMultiSheetEngine({
|
|||||||
}
|
}
|
||||||
|
|
||||||
workbookRef.current.fromJSON(initialData)
|
workbookRef.current.fromJSON(initialData)
|
||||||
|
loadedDataRef.current = dataKey // Сохраняем ключ загруженных данных
|
||||||
safeRecalc()
|
safeRecalc()
|
||||||
} else {
|
} else {
|
||||||
// Если данных нет, просто обновляем ревизию
|
// Если данных нет, просто обновляем ревизию
|
||||||
@@ -244,7 +279,15 @@ export function useMultiSheetEngine({
|
|||||||
const setTemplateData = useCallback(
|
const setTemplateData = useCallback(
|
||||||
(data: Record<string, Record<string, any>>) => {
|
(data: Record<string, Record<string, any>>) => {
|
||||||
templateDataRef.current = JSON.parse(JSON.stringify(data)) // Глубокая копия
|
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) {
|
if (normalizedCurrent !== normalizedTemplate) {
|
||||||
// Если текущего значения нет (ячейка очищена), отправляем ''
|
// Если текущего значения нет (ячейка очищена), отправляем ''
|
||||||
modifiedSheet[cellAddress] = currentVal
|
modifiedSheet[cellAddress] = currentVal
|
||||||
// console.log(
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
// `🔧 Пользовательское изменение ${sheetName}:${cellAddress}:`,
|
console.log(
|
||||||
// {
|
`🔧 Пользовательское изменение ${sheetName}:${cellAddress}:`,
|
||||||
// template: normalizedTemplate,
|
{
|
||||||
// current: normalizedCurrent,
|
template: normalizedTemplate,
|
||||||
// value: currentVal,
|
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
|
return modifiedData
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
332
verification-conditions-api.md
Normal file
332
verification-conditions-api.md
Normal file
@@ -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<string, any> // 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` - Внутренняя ошибка сервера
|
||||||
12
yarn.lock
12
yarn.lock
@@ -379,6 +379,18 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30"
|
||||||
integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==
|
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":
|
"@radix-ui/react-context@1.1.2":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36"
|
||||||
|
|||||||
Reference in New Issue
Block a user