+
{index + 1}
- {standard.shortName}
+ {standard.name}
-
- {standard.accuracy} • {standard.range}
+
+
+
+ {standard.registryNumber}
+
-
-
- {getStandardTypeBadge(standard.type)}
+
+ {standard.range}
))}
diff --git a/src/component/StandardsElement/StandardsSelector.tsx b/src/component/StandardsElement/StandardsSelector.tsx
index 7056c8e..2f84f05 100644
--- a/src/component/StandardsElement/StandardsSelector.tsx
+++ b/src/component/StandardsElement/StandardsSelector.tsx
@@ -6,231 +6,401 @@ import {
DialogHeader,
DialogTitle,
} from '@/component/ui/dialog'
-import { Calendar, Check, FileText, Settings, X } from 'lucide-react'
-import { useState } from 'react'
-
-interface MeasurementStandard {
- id: string
- name: string
- shortName: string
- type: string
- registryNumber: string
- range: string
- accuracy: string
- certificateNumber: string
- validUntil: string
-}
+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
- registryNumber?: string
+ maxItems?: number // Добавляем опциональный параметр
}
-// Моковые данные для эталонов
-const mockStandards: MeasurementStandard[] = [
- {
- id: '1',
- name: 'Эталон массы 1 кг',
- shortName: 'ЭМ-1кг',
- type: 'Масса',
- registryNumber: 'ГРСИ 12345-01',
- range: '0.5-2 кг',
- accuracy: '±0.001 г',
- certificateNumber: 'СИ-2024-001',
- validUntil: '2025-12-31',
- },
- {
- id: '2',
- name: 'Эталон длины 1 м',
- shortName: 'ЭД-1м',
- type: 'Длина',
- registryNumber: 'ГРСИ 12345-02',
- range: '0.5-2 м',
- accuracy: '±0.001 мм',
- certificateNumber: 'СИ-2024-002',
- validUntil: '2025-06-30',
- },
- {
- id: '3',
- name: 'Эталон температуры 20°C',
- shortName: 'ЭТ-20°C',
- type: 'Температура',
- registryNumber: 'ГРСИ 12345-03',
- range: '15-25°C',
- accuracy: '±0.01°C',
- certificateNumber: 'СИ-2024-003',
- validUntil: '2025-03-15',
- },
- {
- id: '4',
- name: 'Эталон давления 1 Па',
- shortName: 'ЭД-1Па',
- type: 'Давление',
- registryNumber: 'ГРСИ 12345-04',
- range: '0.5-2 Па',
- accuracy: '±0.001 Па',
- certificateNumber: 'СИ-2024-004',
- validUntil: '2025-09-20',
- },
- {
- id: '5',
- name: 'Эталон времени 1 с',
- shortName: 'ЭВ-1с',
- type: 'Время',
- registryNumber: 'ГРСИ 12345-05',
- range: '0.5-2 с',
- accuracy: '±0.000001 с',
- certificateNumber: 'СИ-2024-005',
- validUntil: '2025-12-01',
- },
-]
-
export function StandardsSelector({
isOpen,
onClose,
value = [],
onChange,
- registryNumber = 'ГРСИ 12345',
+ maxItems = 7, // Значение по умолчанию
}: StandardsSelectorProps) {
const [selectedStandards, setSelectedStandards] = useState
(value)
+ const [searchTerm, setSearchTerm] = useState('')
+ const { error } = useToast()
+ const searchInputRef = useRef(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 < 7) {
+ } else if (prev.length < maxItems) {
return [...prev, standardId]
+ } else {
+ error(`Можно выбрать максимум ${maxItems} эталонов`, {
+ title: 'Достигнут лимит',
+ })
+ return prev
}
- return prev
})
}
+ const removeStandard = (standardId: string) => {
+ setSelectedStandards(prev => prev.filter(id => id !== standardId))
+ }
+
const handleSave = () => {
onChange(selectedStandards)
onClose()
}
const handleClose = () => {
- setSelectedStandards(value) // Сбрасываем к исходному значению
+ setSelectedStandards(value)
+ setSearchTerm('')
onClose()
}
- const isExpiringSoon = (validUntil: string) => {
- const expiryDate = new Date(validUntil)
- const now = new Date()
- const monthsUntilExpiry =
- (expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24 * 30)
- return monthsUntilExpiry <= 3
+ const renderStandardCard = (
+ standard: Standard,
+ isSelected: boolean,
+ showRemoveButton = false
+ ) => {
+ const isDisabled = !isSelected && isMaxReached && !showRemoveButton
+
+ return (
+ toggleStandard(standard.id)
+ : undefined
+ }
+ >
+ {!showRemoveButton && (
+
+ {isSelected && (
+
+ )}
+
+ )}
+
+
+
+
+ {standard.protocol_name}
+
+
+ {standard.name}
+
+
+
+
+
+
+
+ {standard.registry_number}
+
+
+ {standard.range}
+
+
+
+ {showRemoveButton && (
+
+ )}
+
+ )
+ }
+
+ if (isLoading) {
+ return (
+
+ )
+ }
+
+ if (apiError) {
+ return (
+
+ )
}
return (