почти рабочий вариант

This commit is contained in:
2025-07-24 19:56:58 +03:00
parent f9e7905d55
commit 1f02136455
29 changed files with 1627 additions and 3205 deletions

View File

@@ -0,0 +1,118 @@
import { Button } from '@/component/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/component/ui/select'
import { Info, Settings } from 'lucide-react'
import { useState } from 'react'
import { StandardsSelector } from './StandardsSelector'
import { StandardsConfig } from './definition'
interface StandardsEditorProps {
config: StandardsConfig
onChange: (config: StandardsConfig) => void
}
export const StandardsEditor: React.FC<StandardsEditorProps> = ({
config,
onChange,
}) => {
const [isStandardsDialogOpen, setIsStandardsDialogOpen] = useState(false)
// Обеспечиваем значения по умолчанию
const safeConfig = {
maxItems: config.maxItems || 7,
targetCells: config.targetCells || [],
selectedStandards: config.selectedStandards || [],
}
const updateConfig = (updates: Partial<StandardsConfig>) => {
onChange({ ...safeConfig, ...updates })
}
return (
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center gap-2">
<label className="text-sm font-medium">Максимум эталонов</label>
<Select
value={safeConfig.maxItems.toString()}
onValueChange={value => updateConfig({ maxItems: parseInt(value) })}
>
<SelectTrigger className="h-8 w-16">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="3">3</SelectItem>
<SelectItem value="7">7</SelectItem>
<SelectItem value="9">9</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="12">12</SelectItem>
</SelectContent>
</Select>
</div>
<p className="text-xs text-gray-500">
Максимальное количество эталонов для выбора (0-12)
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Предустановленные эталоны</label>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setIsStandardsDialogOpen(true)}
className="flex-1"
>
<Settings className="mr-2 h-4 w-4" />
{safeConfig.selectedStandards.length > 0
? `Выбрано: ${safeConfig.selectedStandards.length}`
: 'Выбрать эталоны'}
</Button>
{safeConfig.selectedStandards.length > 0 && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => updateConfig({ selectedStandards: [] })}
>
Очистить
</Button>
)}
</div>
<p className="text-xs text-gray-500">
Эталоны, которые будут выбраны по умолчанию при создании протокола
</p>
</div>
<div className="rounded-lg bg-blue-50 p-3">
<div className="flex items-start gap-2">
<Info className="mt-0.5 h-4 w-4 text-blue-600" />
<div className="text-sm">
<p className="font-medium text-blue-900">Как это работает:</p>
<ul className="mt-1 list-disc space-y-1 pl-4 text-blue-800">
<li>Выберите максимальное количество эталонов</li>
<li>Настройте предустановленные эталоны (опционально)</li>
<li>При создании протокола пользователь сможет изменить выбор</li>
</ul>
</div>
</div>
</div>
<StandardsSelector
isOpen={isStandardsDialogOpen}
onClose={() => setIsStandardsDialogOpen(false)}
value={safeConfig.selectedStandards}
onChange={standardIds => {
updateConfig({ selectedStandards: standardIds })
}}
maxItems={safeConfig.maxItems}
/>
</div>
)
}

View File

@@ -0,0 +1,52 @@
import { Button } from '@/component/ui/button'
import { Label } from '@/component/ui/label'
import { FileText, Settings } from 'lucide-react'
import { StandardsConfig } from './definition'
interface StandardsPreviewProps {
config: StandardsConfig
}
const StandardSkeleton = ({ index }: { index: number }) => (
<div className="flex items-center gap-2 rounded-md bg-muted/30 p-2 text-sm">
<div className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-primary/10">
<span className="text-xs font-medium text-primary">{index + 1}</span>
</div>
<div className="min-w-0 flex-1">
<div className="mb-1 h-3 w-3/4 rounded bg-muted"></div>
<div className="mt-0.5 flex items-center gap-1 text-xs text-muted-foreground">
<FileText className="h-2.5 w-2.5" />
<div className="h-2 w-16 rounded bg-muted"></div>
</div>
</div>
<div className="h-5 w-12 shrink-0 rounded bg-muted"></div>
</div>
)
export const StandardsPreview: React.FC<StandardsPreviewProps> = ({
config,
}) => {
const skeletonCount = config.maxItems || 7
return (
<div className="space-y-2">
<div>
<div className="mb-2 flex items-center justify-between">
<Label className="text-sm font-medium">Эталоны</Label>
<Button variant="outline" size="sm" disabled className="h-7 px-2">
<Settings className="mr-1 h-3 w-3" />
Настроить
</Button>
</div>
<div className="space-y-2">
<div className="space-y-1">
{Array.from({ length: skeletonCount }, (_, index) => (
<StandardSkeleton key={index} index={index} />
))}
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,409 @@
import { Badge } from '@/component/ui/badge'
import { Button } from '@/component/ui/button'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/component/ui/dialog'
import { Input } from '@/component/ui/input'
import { Separator } from '@/component/ui/separator'
import { useToast } from '@/lib/hooks/useToast'
import {
Check,
ExternalLink,
FileText,
Search,
Settings,
X,
} from 'lucide-react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useStandards } from './hooks'
import { Standard } from './types'
interface StandardsSelectorProps {
isOpen: boolean
onClose: () => void
value: string[]
onChange: (value: string[]) => void
maxItems?: number // Добавляем опциональный параметр
}
export function StandardsSelector({
isOpen,
onClose,
value = [],
onChange,
maxItems = 7, // Значение по умолчанию
}: StandardsSelectorProps) {
const [selectedStandards, setSelectedStandards] = useState<string[]>(value)
const [searchTerm, setSearchTerm] = useState('')
const { error } = useToast()
const searchInputRef = useRef<HTMLInputElement>(null)
const { data: standards = [], isLoading, error: apiError } = useStandards()
// Автофокус на поле поиска при открытии
useEffect(() => {
if (isOpen && searchInputRef.current) {
const timer = setTimeout(() => {
searchInputRef.current?.focus()
const closeButton = document.querySelector(
'[data-state="open"] button[aria-label="Close"]'
)
if (closeButton && document.activeElement === closeButton) {
searchInputRef.current?.focus()
}
}, 150)
return () => clearTimeout(timer)
}
}, [isOpen])
// Обработка клавиш для автоматического ввода в поиск
useEffect(() => {
if (!isOpen) return
const handleKeyDown = (event: KeyboardEvent) => {
const activeElement = document.activeElement
if (
activeElement?.tagName === 'INPUT' ||
activeElement?.tagName === 'TEXTAREA' ||
activeElement?.tagName === 'BUTTON' ||
activeElement?.getAttribute('contenteditable') === 'true'
) {
return
}
if (
event.ctrlKey ||
event.metaKey ||
event.altKey ||
event.key === 'Tab' ||
event.key === 'Enter' ||
event.key === 'Escape' ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight'
) {
return
}
if (event.key === 'Backspace') {
event.preventDefault()
setSearchTerm(prev => prev.slice(0, -1))
searchInputRef.current?.focus()
return
}
if (event.key.length === 1) {
event.preventDefault()
setSearchTerm(prev => prev + event.key)
searchInputRef.current?.focus()
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [isOpen])
const filteredStandards = useMemo(() => {
if (!searchTerm) return standards
const searchLower = searchTerm.toLowerCase()
return standards.filter(
standard =>
standard.name.toLowerCase().includes(searchLower) ||
standard.protocol_name.toLowerCase().includes(searchLower) ||
standard.registry_number.toLowerCase().includes(searchLower)
)
}, [searchTerm, standards])
const selectedStandardsData = standards.filter(standard =>
selectedStandards.includes(standard.id)
)
const unselectedStandards = filteredStandards.filter(
standard => !selectedStandards.includes(standard.id)
)
const isMaxReached = selectedStandards.length >= maxItems
const toggleStandard = (standardId: string) => {
setSelectedStandards(prev => {
if (prev.includes(standardId)) {
return prev.filter(id => id !== standardId)
} else if (prev.length < maxItems) {
return [...prev, standardId]
} else {
error(`Можно выбрать максимум ${maxItems} эталонов`, {
title: 'Достигнут лимит',
})
return prev
}
})
}
const removeStandard = (standardId: string) => {
setSelectedStandards(prev => prev.filter(id => id !== standardId))
}
const handleSave = () => {
onChange(selectedStandards)
onClose()
}
const handleClose = () => {
setSelectedStandards(value)
setSearchTerm('')
onClose()
}
const renderStandardCard = (
standard: Standard,
isSelected: boolean,
showRemoveButton = false
) => {
const isDisabled = !isSelected && isMaxReached && !showRemoveButton
return (
<div
key={standard.id}
className={`flex items-center gap-3 rounded-md border px-3 py-2 transition-all duration-200 ${
isSelected
? 'border-primary bg-primary/10 shadow-sm'
: isDisabled
? 'border-border bg-muted/50 opacity-60'
: 'border-border hover:border-primary/50 hover:bg-primary/5'
} ${!showRemoveButton && !isDisabled ? 'cursor-pointer' : isDisabled ? 'cursor-not-allowed' : ''}`}
onClick={
!showRemoveButton && !isDisabled
? () => toggleStandard(standard.id)
: undefined
}
>
{!showRemoveButton && (
<div
className={`flex h-4 w-4 items-center justify-center rounded border transition-colors ${
isSelected
? 'border-primary bg-primary'
: isDisabled
? 'border-muted-foreground/30 bg-muted'
: 'border-border'
}`}
>
{isSelected && (
<Check className="h-3 w-3 text-primary-foreground" />
)}
</div>
)}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-3">
<div
className={`min-w-20 shrink-0 text-sm font-medium ${
isDisabled ? 'text-muted-foreground' : 'text-foreground'
}`}
>
{standard.protocol_name}
</div>
<div
className={`truncate text-sm ${
isDisabled
? 'text-muted-foreground/70'
: 'text-muted-foreground'
}`}
>
{standard.name}
</div>
</div>
</div>
<div
className={`flex shrink-0 items-center gap-4 text-xs ${
isDisabled ? 'text-muted-foreground/50' : 'text-muted-foreground'
}`}
>
<div className="flex items-center gap-1">
<FileText className="h-3 w-3" />
<span className="hidden sm:inline">{standard.registry_number}</span>
</div>
<Badge variant="secondary" className="px-1.5 py-0.5 text-xs">
{standard.range}
</Badge>
</div>
{showRemoveButton && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 shrink-0 p-0 text-muted-foreground hover:text-destructive"
onClick={() => removeStandard(standard.id)}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
)
}
if (isLoading) {
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="flex max-h-[95vh] max-w-6xl flex-col overflow-hidden">
<div className="flex h-64 items-center justify-center">
<div className="text-center">
<div className="mx-auto mb-2 h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<p className="text-muted-foreground">Загрузка эталонов...</p>
</div>
</div>
</DialogContent>
</Dialog>
)
}
if (apiError) {
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="flex max-h-[95vh] max-w-6xl flex-col overflow-hidden">
<div className="flex h-64 items-center justify-center">
<div className="text-center">
<p className="mb-2 text-destructive">Ошибка загрузки эталонов</p>
<p className="text-sm text-muted-foreground">
{apiError.message}
</p>
</div>
</div>
</DialogContent>
</Dialog>
)
}
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="flex max-h-[95vh] max-w-6xl flex-col overflow-hidden">
<DialogHeader className="shrink-0 pb-3">
<div className="flex items-center justify-between">
<DialogTitle className="flex items-center gap-2 text-lg">
<Settings className="h-5 w-5" />
Измерительные эталоны
<span
className={`text-sm font-normal ${
isMaxReached ? 'text-orange-600' : 'text-muted-foreground'
}`}
>
({selectedStandards.length}/{maxItems})
</span>
</DialogTitle>
</div>
</DialogHeader>
<div className="flex flex-1 flex-col space-y-4 overflow-hidden">
{/* Поиск и ссылка на управление */}
<div className="flex shrink-0 gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
ref={searchInputRef}
placeholder="Начните печатать для поиска..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="pl-10"
autoComplete="off"
autoFocus
/>
{searchTerm && (
<Button
variant="ghost"
size="sm"
className="absolute right-2 top-1/2 h-6 w-6 -translate-y-1/2 p-0"
onClick={() => {
setSearchTerm('')
searchInputRef.current?.focus()
}}
>
<X className="h-3 w-3" />
</Button>
)}
</div>
<Button
variant="outline"
onClick={() => {
// Открыть страницу управления эталонами
window.open('/standards-management', '_blank')
}}
className="shrink-0"
>
<ExternalLink className="mr-2 h-4 w-4" />
Управление
</Button>
</div>
{/* Контент с прокруткой */}
<div className="flex-1 space-y-4 overflow-y-auto">
{/* Выбранные эталоны */}
{selectedStandardsData.length > 0 && (
<div>
<h4 className="mb-3 text-sm font-medium text-foreground">
Выбранные эталоны ({selectedStandardsData.length})
</h4>
<div className="max-h-64 space-y-1 overflow-y-auto">
{selectedStandardsData.map(standard =>
renderStandardCard(standard, true, true)
)}
</div>
</div>
)}
{selectedStandardsData.length > 0 &&
unselectedStandards.length > 0 && <Separator />}
{/* Доступные эталоны */}
{unselectedStandards.length > 0 && (
<div>
<h4 className="mb-3 text-sm font-medium text-foreground">
Доступные эталоны ({unselectedStandards.length})
{isMaxReached && (
<span className="ml-2 text-xs text-orange-600">
Достигнут лимит выбора
</span>
)}
</h4>
<div className="space-y-1">
{unselectedStandards.map(standard =>
renderStandardCard(standard, false)
)}
</div>
</div>
)}
{filteredStandards.length === 0 && searchTerm && (
<div className="py-8 text-center text-muted-foreground">
Эталоны не найдены по запросу "{searchTerm}"
</div>
)}
{standards.length === 0 && !searchTerm && (
<div className="py-8 text-center text-muted-foreground">
<Settings className="mx-auto mb-2 h-8 w-8 opacity-50" />
Эталоны не созданы
</div>
)}
</div>
</div>
{/* Зафиксированные кнопки */}
<div className="mt-4 shrink-0 border-t pt-3">
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={handleClose} size="sm">
Отмена
</Button>
<Button onClick={handleSave} size="sm">
Сохранить
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,79 @@
import {
CreateStandardInput,
CreateStandardOutput,
GetStandardOutput,
GetStandardsOutput,
PatchStandardInput,
PatchStandardOutput,
} from './types'
const BASE_URL = '/api'
// Получить все эталоны
export const getStandards = async (): Promise<GetStandardsOutput> => {
console.log('getStandards', BASE_URL)
const response = await fetch(`${BASE_URL}/standards/`)
if (!response.ok) {
throw new Error('Failed to fetch standards')
}
return response.json()
}
// Получить один эталон
export const getStandard = async (
standardId: string
): Promise<GetStandardOutput> => {
const response = await fetch(`${BASE_URL}/standards/${standardId}/`)
if (!response.ok) {
throw new Error('Failed to fetch standard')
}
return response.json()
}
// Создать эталон
export const createStandard = async (
input: CreateStandardInput
): Promise<CreateStandardOutput> => {
const response = await fetch(`${BASE_URL}/standards/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
})
if (!response.ok) {
throw new Error('Failed to create standard')
}
return response.json()
}
// Обновить эталон
export const patchStandard = async (
standardId: string,
input: PatchStandardInput
): Promise<PatchStandardOutput> => {
const response = await fetch(`${BASE_URL}/standards/${standardId}/`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
})
if (!response.ok) {
throw new Error('Failed to update standard')
}
return response.json()
}
// Удалить эталон
export const deleteStandard = async (
standardId: string
): Promise<{ [key: string]: string }> => {
const response = await fetch(`${BASE_URL}/standards/${standardId}/`, {
method: 'DELETE',
})
if (!response.ok) {
throw new Error('Failed to delete standard')
}
return response.json()
}

View File

@@ -0,0 +1,188 @@
import { Badge } from '@/component/ui/badge'
import { Button } from '@/component/ui/button'
import { Label } from '@/component/ui/label'
import { ElementDefinition } from '@/entitiy/element/model/interface'
import { CellTarget } from '@/type/template'
import { FileText, Settings, Weight } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useStandards } from './hooks'
import { StandardsEditor } from './StandardsEditor'
import { StandardsPreview } from './StandardsPreview'
import { StandardsSelector } from './StandardsSelector'
export interface StandardsConfig {
maxItems: number
targetCells: CellTarget[]
selectedStandards?: string[]
name?: string
}
// Новый интерфейс для значения элемента эталонов
export interface StandardsValue {
standardIds: string[]
standardNames: string[] // пронумерованные protocol_name для записи в ячейки (1. Name, 2. Name)
}
export const standardsDefinition: ElementDefinition<
StandardsConfig,
StandardsValue
> = {
type: 'standards',
label: 'Выбор эталонов',
icon: <Weight className="h-4 w-4" />,
version: 1,
defaultConfig: {
maxItems: 7,
targetCells: [],
selectedStandards: [],
name: '',
},
Editor: StandardsEditor,
Preview: StandardsPreview,
Render: ({ config, value, onChange }) => {
const [isStandardsDialogOpen, setIsStandardsDialogOpen] = useState(false)
const [selectedStandardIds, setSelectedStandardIds] = useState<string[]>(
// Инициализируем из value.standardIds, config.selectedStandards или пустого массива
value?.standardIds || config.selectedStandards || []
)
// Получаем реальные данные эталонов
const { data: standards = [] } = useStandards()
// Синхронизируем selectedStandardIds с value и config
useEffect(() => {
const standardIds = value?.standardIds || config.selectedStandards || []
setSelectedStandardIds(standardIds)
}, [value?.standardIds, config.selectedStandards])
// Получаем данные выбранных эталонов из реального API
const selectedStandardsData = selectedStandardIds
.map(id => standards.find(s => s.id === id))
.filter(Boolean)
// Автоматическая инициализация onChange при загрузке данных эталонов
useEffect(() => {
if (standards.length > 0 && selectedStandardIds.length > 0) {
const standardNames = selectedStandardIds
.map((id, index) => {
const standard = standards.find(s => s.id === id)
return standard?.protocol_name
? `${index + 1}. ${standard.protocol_name}`
: ''
})
.filter(Boolean)
onChange?.({
standardIds: selectedStandardIds,
standardNames,
})
}
}, [selectedStandardIds, standards, onChange])
return (
<div className="space-y-2">
{/* Блок с измерительными эталонами */}
<div>
<div className="mb-2 flex items-center justify-between">
<Label className="text-sm font-medium">Эталоны</Label>
<Button
variant="outline"
size="sm"
onClick={() => setIsStandardsDialogOpen(true)}
className="h-7 px-2"
>
<Settings className="mr-1 h-3 w-3" />
Настроить
</Button>
</div>
<div className="space-y-2">
{selectedStandardsData.length > 0 ? (
<div className="space-y-1">
{selectedStandardsData.map(
(standard, index) =>
standard && (
<div
key={standard.id}
className="flex items-center gap-2 rounded-md bg-muted/30 p-2 text-sm"
>
<div className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-primary/10">
<span className="text-xs font-medium text-primary">
{index + 1}
</span>
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-xs font-medium text-foreground">
{standard.name}
</div>
<div className="mt-0.5 flex items-center gap-1 text-xs text-muted-foreground">
<FileText className="h-2.5 w-2.5" />
<span className="hidden text-xs sm:inline">
{standard.registry_number}
</span>
</div>
</div>
<Badge
variant="secondary"
className="shrink-0 px-1.5 py-0.5 text-xs"
>
{standard.range}
</Badge>
</div>
)
)}
</div>
) : (
<div className="rounded-md border border-dashed bg-muted/30 p-3 text-center text-sm text-muted-foreground">
<Settings className="mx-auto mb-1 h-4 w-4 opacity-50" />
Эталоны не выбраны
</div>
)}
</div>
</div>
<StandardsSelector
isOpen={isStandardsDialogOpen}
onClose={() => setIsStandardsDialogOpen(false)}
value={selectedStandardIds}
onChange={standardIds => {
setSelectedStandardIds(standardIds)
// Получаем protocol_name эталонов для выбранных ID с нумерацией
const standardNames = standardIds
.map((id, index) => {
const standard = standards.find(s => s.id === id)
return standard?.protocol_name
? `${index + 1}. ${standard.protocol_name}`
: ''
})
.filter(Boolean)
// Сохраняем полную информацию: ID и protocol_name
onChange?.({
standardIds,
standardNames,
})
}}
maxItems={config.maxItems}
/>
</div>
)
},
mapToCells: cfg => {
// Возвращаем ячейки из конфигурации
return cfg.targetCells || []
},
mapToCellValues: (cfg, value) => {
// Используем targetCells из конфигурации
const cells = cfg.targetCells || []
// value теперь содержит как ID, так и пронумерованные protocol_name эталонов
const standardNames = value?.standardNames || []
return cells.map((target, idx) => ({
target,
value: standardNames[idx] || '',
}))
},
}

View File

@@ -0,0 +1,83 @@
import { useToast } from '@/lib/hooks/useToast'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import * as api from './api'
import { CreateStandardInput, PatchStandardInput } from './types'
// Ключи для React Query
export const standardsKeys = {
all: ['standards'] as const,
detail: (id: string) => ['standards', id] as const,
}
// Получить все эталоны
export const useStandards = () => {
return useQuery({
queryKey: standardsKeys.all,
queryFn: api.getStandards,
select: data => data.standards,
})
}
// Получить один эталон
export const useStandard = (standardId: string) => {
return useQuery({
queryKey: standardsKeys.detail(standardId),
queryFn: () => api.getStandard(standardId),
select: data => data.standard,
enabled: !!standardId,
})
}
// Создать эталон
export const useCreateStandard = () => {
const queryClient = useQueryClient()
const { success, error } = useToast()
return useMutation({
mutationFn: (input: CreateStandardInput) => api.createStandard(input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: standardsKeys.all })
success('Эталон успешно создан')
},
onError: (err: Error) => {
error(`Ошибка создания эталона: ${err.message}`)
},
})
}
// Обновить эталон
export const useUpdateStandard = () => {
const queryClient = useQueryClient()
const { success, error } = useToast()
return useMutation({
mutationFn: ({ id, input }: { id: string; input: PatchStandardInput }) =>
api.patchStandard(id, input),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: standardsKeys.all })
queryClient.invalidateQueries({ queryKey: standardsKeys.detail(id) })
success('Эталон успешно обновлен')
},
onError: (err: Error) => {
error(`Ошибка обновления эталона: ${err.message}`)
},
})
}
// Удалить эталон
export const useDeleteStandard = () => {
const queryClient = useQueryClient()
const { success, error } = useToast()
return useMutation({
mutationFn: (standardId: string) => api.deleteStandard(standardId),
onSuccess: (_, standardId) => {
queryClient.invalidateQueries({ queryKey: standardsKeys.all })
queryClient.removeQueries({ queryKey: standardsKeys.detail(standardId) })
success('Эталон успешно удален')
},
onError: (err: Error) => {
error(`Ошибка удаления эталона: ${err.message}`)
},
})
}

View File

@@ -0,0 +1,47 @@
// Типы на основе API схемы
export interface Standard {
id: string
name: string
protocol_name: string // было shortName
registry_number: string
range: string
accuracy: string
valid_until: string // date format
created_at: string // datetime
updated_at: string // datetime
deleted_at: string | null // datetime or null
}
export interface CreateStandardInput {
name: string
protocol_name: string
registry_number: string
range: string
accuracy: string
valid_until: string // date format
}
export interface CreateStandardOutput {
id: string
}
export interface PatchStandardInput {
name?: string | null
protocol_name?: string | null
registry_number?: string | null
range?: string | null
accuracy?: string | null
valid_until?: string | null
}
export interface PatchStandardOutput {
id: string
}
export interface GetStandardOutput {
standard: Standard | null
}
export interface GetStandardsOutput {
standards: Standard[]
}

View File

@@ -1,11 +1,10 @@
import { textDefinition } from '@/component/BasicElements'
import { calibrationConditionsDefinition } from '@/component/BasicElements/CalibrationConditionsElement'
import { numberDefinition } from '@/component/BasicElements/NumberElement'
import { selectDefinition } from '@/component/BasicElements/SelectElement'
import { standardsDefinition } from '@/component/StandardsElement/definition'
import { verificationConditionsDefinition } from '@/component/VerificationConditionsElement/definition'
import { verificationConditionsElementDefinition as verificationConditionsDefinition } from '@/component/VerificationConditionsElement/definition'
import { buttonGroupDefinition } from '@/entitiy/element/model/implementations/ButtonGroup'
import { dateDefinition } from '@/entitiy/element/model/implementations/DateElement'
import { standardsDefinition } from '@/entitiy/element/model/implementations/StandardsElement/definition'
import { CellTarget } from '@/type/template'
import { ReactNode } from 'react'
@@ -67,7 +66,6 @@ export const elementDefinitions = {
number: numberDefinition,
date: dateDefinition,
standards: standardsDefinition,
calibration_conditions: calibrationConditionsDefinition,
verification_conditions: verificationConditionsDefinition,
button_group: buttonGroupDefinition,
} as const
@@ -80,7 +78,6 @@ export function initializeElementRegistry() {
registerElement('number', numberDefinition)
registerElement('date', dateDefinition)
registerElement('button_group', buttonGroupDefinition)
registerElement('calibration_conditions', calibrationConditionsDefinition)
registerElement('standards', standardsDefinition)
registerElement('verification_conditions', verificationConditionsDefinition)
}