баг при смене страницы
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user