From 66b94cdd8f794d272537a2b590c8a42c0f315b0e Mon Sep 17 00:00:00 2001 From: tlartem Date: Thu, 24 Jul 2025 09:10:08 +0300 Subject: [PATCH] =?UTF-8?q?=D0=AD=D1=82=D0=B0=D0=BB=D0=BE=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/App.tsx | 5 + .../StandardsElement/StandardsDialog.tsx | 377 ++++++++++--- .../StandardsElement/StandardsEditor.tsx | 62 +- .../StandardsElement/StandardsPreview.tsx | 95 +--- .../StandardsElement/StandardsSelector.tsx | 530 ++++++++++++------ src/component/StandardsElement/api.ts | 79 +++ src/component/StandardsElement/definition.tsx | 146 ++--- src/component/StandardsElement/hooks.ts | 83 +++ .../StandardsElement/mockStandards.ts | 229 ++++++++ src/component/StandardsElement/types.ts | 47 ++ src/page/StandardsManagementPage.tsx | 485 ++++++++++++++++ vite.config.ts | 3 +- 12 files changed, 1674 insertions(+), 467 deletions(-) create mode 100644 src/component/StandardsElement/api.ts create mode 100644 src/component/StandardsElement/hooks.ts create mode 100644 src/component/StandardsElement/mockStandards.ts create mode 100644 src/component/StandardsElement/types.ts create mode 100644 src/page/StandardsManagementPage.tsx diff --git a/src/app/App.tsx b/src/app/App.tsx index 0d31aa0..d56e3ce 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -11,6 +11,7 @@ import { } from '@/lib/hooks/useToast' import { ElementsCreation } from '@/page/ElementsCreation' import { ProtocolCreation } from '@/page/ProtocolCreation' +import { StandardsManagementPage } from '@/page/StandardsManagementPage' import { TemplateEditPage } from '@/page/TemplateEditPage' import { TemplatesOverviewPage } from '@/page/TemplatesOverviewPage' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' @@ -52,6 +53,10 @@ const App: FC = () => { path="templates/:templateId/protocols" element={} /> + } + /> diff --git a/src/component/StandardsElement/StandardsDialog.tsx b/src/component/StandardsElement/StandardsDialog.tsx index 2b79ae7..9353afa 100644 --- a/src/component/StandardsElement/StandardsDialog.tsx +++ b/src/component/StandardsElement/StandardsDialog.tsx @@ -1,9 +1,10 @@ -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 { Separator } from '@/component/ui/separator' -import { Calendar, Check, FileText, Settings } from 'lucide-react' -import { useState } from 'react' +import { useToast } from '@/lib/hooks/useToast' +import { Check, FileText, Gauge, Search, Settings, X } from 'lucide-react' +import { useEffect, useMemo, useRef, useState } from 'react' interface MeasurementStandard { id: string @@ -29,7 +30,6 @@ interface StandardsDialogProps { standards: MeasurementStandard[] config: StandardsConfig onSave: (config: StandardsConfig) => void - registryNumber: string } export function StandardsDialog({ @@ -38,25 +38,121 @@ export function StandardsDialog({ standards, config, onSave, - registryNumber, }: StandardsDialogProps) { const [selectedStandards, setSelectedStandards] = useState( config.selectedStandards ) + const [searchTerm, setSearchTerm] = useState('') + const { error } = useToast() + const searchInputRef = useRef(null) + + // Автофокус на поле поиска при открытии + useEffect(() => { + if (isOpen && searchInputRef.current) { + // Небольшая задержка для корректной работы с анимацией диалога + const timer = setTimeout(() => { + 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 + } + + // Обработка Backspace для удаления последнего символа + 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]) if (!isOpen) return null + const filteredStandards = useMemo(() => { + if (!searchTerm) return standards + + const searchLower = searchTerm.toLowerCase() + return standards.filter( + standard => + standard.name.toLowerCase().includes(searchLower) || + standard.shortName.toLowerCase().includes(searchLower) || + standard.type.toLowerCase().includes(searchLower) || + standard.registryNumber.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 >= 7 + const toggleStandard = (standardId: string) => { setSelectedStandards(prev => { if (prev.includes(standardId)) { return prev.filter(id => id !== standardId) } else if (prev.length < 7) { return [...prev, standardId] + } else { + error('Можно выбрать максимум 7 эталонов', { + title: 'Достигнут лимит', + }) + return prev } - return prev }) } + const removeStandard = (standardId: string) => { + setSelectedStandards(prev => prev.filter(id => id !== standardId)) + } + const handleSave = () => { onSave({ ...config, @@ -66,6 +162,12 @@ export function StandardsDialog({ onClose() } + const handleClose = () => { + setSelectedStandards(config.selectedStandards) // Сбрасываем к исходному значению + setSearchTerm('') + onClose() + } + const isExpiringSoon = (validUntil: string) => { const expiryDate = new Date(validUntil) const now = new Date() @@ -74,109 +176,200 @@ export function StandardsDialog({ return monthsUntilExpiry <= 3 } + const renderStandardCard = ( + standard: MeasurementStandard, + isSelected: boolean, + showRemoveButton = false + ) => { + const isDisabled = !isSelected && isMaxReached && !showRemoveButton + + return ( +
toggleStandard(standard.id) + : undefined + } + > + {!showRemoveButton && ( +
+ {isSelected && ( + + )} +
+ )} + +
+
+
+ {standard.shortName} +
+
+ {standard.name} +
+
+
+ +
+
+ + {standard.registryNumber} +
+
+ + {standard.range} +
+
+ + {showRemoveButton && ( + + )} +
+ ) + } + return (
- - + +
Измерительные эталоны + + ({selectedStandards.length}/7) + -

- ГРСИ: {registryNumber} • Выбрано: {selectedStandards.length}/7 -

- -
- {standards.map(standard => { - const isSelected = selectedStandards.includes(standard.id) - const isExpiring = isExpiringSoon(standard.validUntil) - - return ( -
toggleStandard(standard.id)} - className={`cursor-pointer rounded-lg border p-3 transition-all duration-200 ${ - isSelected - ? 'border-primary bg-primary/10 shadow-sm' - : 'border-border hover:border-primary/50 hover:bg-primary/5' - }`} - > -
-
- {isSelected && ( - - )} -
- -
-
-
-
- {standard.shortName} -
-
- {standard.name} -
-
- -
- - {standard.accuracy} - - {isExpiring && ( - - - Истекает - - )} -
-
- -
-
- - {standard.registryNumber} -
-
- Диапазон: {standard.range} -
-
- Действует до:{' '} - {new Date(standard.validUntil).toLocaleDateString( - 'ru-RU' - )} -
-
-
-
-
- ) - })} + + {/* Поиск */} +
+ + setSearchTerm(e.target.value)} + className="pl-10" + autoComplete="off" + autoFocus + /> + {searchTerm && ( + + )}
- + {/* Контент с прокруткой */} +
+ {/* Выбранные эталоны */} + {selectedStandardsData.length > 0 && ( +
+

+ Выбранные эталоны ({selectedStandardsData.length}) +

+
+ {selectedStandardsData.map(standard => + renderStandardCard(standard, true, true) + )} +
+
+ )} -
-
- Максимум 7 эталонов. Выбрано: {selectedStandards.length} -
-
-
+ + + + {/* Зафиксированные кнопки */} +
+
+ - +
diff --git a/src/component/StandardsElement/StandardsEditor.tsx b/src/component/StandardsElement/StandardsEditor.tsx index b5abd44..de384b6 100644 --- a/src/component/StandardsElement/StandardsEditor.tsx +++ b/src/component/StandardsElement/StandardsEditor.tsx @@ -1,3 +1,4 @@ +import { Button } from '@/component/ui/button' import { Select, SelectContent, @@ -7,12 +8,15 @@ import { } from '@/component/ui/select' import { CellTarget } from '@/type/template' import { Info, Settings } from 'lucide-react' +import { useState } from 'react' +import { StandardsSelector } from './StandardsSelector' interface StandardsConfig { sheet: string // Лист Excel startCell: string // Первая ячейка (например "A10") maxItems: number // Обычно 7 targetCells: CellTarget[] + selectedStandards?: string[] // Выбранные эталоны (ID) } interface StandardsEditorProps { @@ -24,12 +28,15 @@ export const StandardsEditor: React.FC = ({ config, onChange, }) => { + const [isStandardsDialogOpen, setIsStandardsDialogOpen] = useState(false) + // Обеспечиваем значения по умолчанию const safeConfig = { sheet: config.sheet || 'L', startCell: config.startCell || 'A1', maxItems: config.maxItems || 7, targetCells: config.targetCells || [], + selectedStandards: config.selectedStandards || [], } const updateConfig = (updates: Partial) => { @@ -69,16 +76,57 @@ export const StandardsEditor: React.FC = ({

-
+
+ +
+ + {safeConfig.selectedStandards.length > 0 && ( + + )} +
+

+ Эталоны, которые будут выбраны по умолчанию при создании протокола +

+ + setIsStandardsDialogOpen(false)} + value={safeConfig.selectedStandards} + onChange={standardIds => { + updateConfig({ selectedStandards: standardIds }) + }} + maxItems={safeConfig.maxItems} + /> +
+ +
-
Настройка ячеек
-
- Ячейки для размещения эталонов настраиваются в разделе "Целевые - ячейки" выше. Эталоны будут записываться в указанные ячейки по - порядку. -
+

Как это работает:

+
    +
  • Выберите максимальное количество эталонов
  • +
  • Настройте предустановленные эталоны (опционально)
  • +
  • При создании протокола пользователь сможет изменить выбор
  • +
diff --git a/src/component/StandardsElement/StandardsPreview.tsx b/src/component/StandardsElement/StandardsPreview.tsx index 22484b0..53a7460 100644 --- a/src/component/StandardsElement/StandardsPreview.tsx +++ b/src/component/StandardsElement/StandardsPreview.tsx @@ -1,7 +1,8 @@ import { Badge } from '@/component/ui/badge' import { Button } from '@/component/ui/button' import { Label } from '@/component/ui/label' -import { Settings, Wrench } from 'lucide-react' +import { FileText, Settings } from 'lucide-react' +import { mockStandards } from './mockStandards' interface StandardsConfig { sheet: string @@ -14,77 +15,6 @@ interface StandardsPreviewProps { config: StandardsConfig } -// Моковые данные для эталонов (такие же как в definition.tsx) -const mockStandards = [ - { - 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', - }, -] - -// Функция для получения типа эталона в сокращенном виде -const getStandardTypeBadge = (type: string) => { - switch (type) { - case 'Манометр грузопоршневой': - return 'МГП' - case 'Калибратор давления': - return 'КД' - default: - return 'МО' - } -} - export const StandardsPreview: React.FC = ({ config, }) => { @@ -104,28 +34,33 @@ export const StandardsPreview: React.FC = ({
-
+
{previewStandards.map((standard, index) => (
-
+
{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 ( + + +
+
+

Ошибка загрузки эталонов

+

+ {apiError.message} +

+
+
+
+
+ ) } return ( - - + +
-
- - - Измерительные эталоны - -

- ГРСИ: {registryNumber} • Выбрано: {selectedStandards.length}/7 -

-
- + + + Измерительные эталоны + + ({selectedStandards.length}/{maxItems}) + +
-
-
- {mockStandards.map(standard => { - const isSelected = selectedStandards.includes(standard.id) - const isExpiring = isExpiringSoon(standard.validUntil) - - return ( -
toggleStandard(standard.id)} - className={`cursor-pointer rounded-lg border p-3 transition-all duration-200 ${ - isSelected - ? 'border-primary bg-primary/10 shadow-sm' - : 'border-border hover:border-primary/50 hover:bg-primary/5' - }`} +
+ {/* Поиск и ссылка на управление */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + autoComplete="off" + autoFocus + /> + {searchTerm && ( +
- ) - })} + + + )} +
+
-
-
- Максимум 7 эталонов. Выбрано: {selectedStandards.length} -
-
- - -
+ {/* Контент с прокруткой */} +
+ {/* Выбранные эталоны */} + {selectedStandardsData.length > 0 && ( +
+

+ Выбранные эталоны ({selectedStandardsData.length}) +

+
+ {selectedStandardsData.map(standard => + renderStandardCard(standard, true, true) + )} +
+
+ )} + + {selectedStandardsData.length > 0 && + unselectedStandards.length > 0 && } + + {/* Доступные эталоны */} + {unselectedStandards.length > 0 && ( +
+

+ Доступные эталоны ({unselectedStandards.length}) + {isMaxReached && ( + + Достигнут лимит выбора + + )} +

+
+ {unselectedStandards.map(standard => + renderStandardCard(standard, false) + )} +
+
+ )} + + {filteredStandards.length === 0 && searchTerm && ( +
+ Эталоны не найдены по запросу "{searchTerm}" +
+ )} + + {standards.length === 0 && !searchTerm && ( +
+ + Эталоны не созданы +
+ )} +
+
+ + {/* Зафиксированные кнопки */} +
+
+ +
diff --git a/src/component/StandardsElement/api.ts b/src/component/StandardsElement/api.ts new file mode 100644 index 0000000..f691952 --- /dev/null +++ b/src/component/StandardsElement/api.ts @@ -0,0 +1,79 @@ +import { + CreateStandardInput, + CreateStandardOutput, + GetStandardOutput, + GetStandardsOutput, + PatchStandardInput, + PatchStandardOutput, +} from './types' + +const BASE_URL = '/api' + +// Получить все эталоны +export const getStandards = async (): Promise => { + 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 => { + 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 => { + 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 => { + 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() +} diff --git a/src/component/StandardsElement/definition.tsx b/src/component/StandardsElement/definition.tsx index 5403654..1d1be66 100644 --- a/src/component/StandardsElement/definition.tsx +++ b/src/component/StandardsElement/definition.tsx @@ -3,93 +3,20 @@ 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 { Settings, Weight, Wrench } from 'lucide-react' +import { FileText, Settings, Weight } from 'lucide-react' import { useEffect, useState } from 'react' +import { useStandards } from './hooks' +import { getStandardById } from './mockStandards' import { StandardsEditor } from './StandardsEditor' import { StandardsPreview } from './StandardsPreview' import { StandardsSelector } from './StandardsSelector' -// Моковые данные для эталонов (такие же как в StandardsSelector) -const mockStandards = [ - { - 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', - }, -] - -// Функция для получения данных эталона по ID -const getStandardById = (id: string) => { - return mockStandards.find(standard => standard.id === id) -} - -// Функция для получения типа эталона в сокращенном виде -const getStandardTypeBadge = (type: string) => { - switch (type) { - case 'Манометр грузопоршневой': - return 'МГП' - case 'Калибратор давления': - return 'КД' - default: - return 'МО' - } -} - interface StandardsConfig { sheet: string // Лист Excel startCell: string // Первая ячейка (например "A10") maxItems: number // Обычно 7 targetCells: CellTarget[] + selectedStandards?: string[] // Выбранные эталоны (ID) } export const standardsDefinition: ElementDefinition = @@ -103,30 +30,30 @@ export const standardsDefinition: ElementDefinition = startCell: 'A1', maxItems: 7, targetCells: [], + selectedStandards: [], // По умолчанию пустой массив }, Editor: StandardsEditor, - Preview: StandardsPreview, + Preview: StandardsPreview, // Использует моки для демонстрации Render: ({ config, value, onChange }) => { const [isStandardsDialogOpen, setIsStandardsDialogOpen] = useState(false) const [selectedStandardIds, setSelectedStandardIds] = useState( - [] + // Инициализируем из config.selectedStandards или value + config.selectedStandards || (Array.isArray(value) ? value : []) ) - // Если value содержит названия эталонов, находим соответствующие ID - useEffect(() => { - if (Array.isArray(value) && value.length > 0) { - const ids = value - .map(name => { - const standard = mockStandards.find(s => s.name === name) - return standard?.id || '' - }) - .filter(Boolean) - setSelectedStandardIds(ids) - } - }, [value]) + // Получаем реальные данные эталонов + const { data: standards = [] } = useStandards() + // Синхронизируем selectedStandardIds с config и value + useEffect(() => { + const standards = + config.selectedStandards || (Array.isArray(value) ? value : []) + setSelectedStandardIds(standards) + }, [config.selectedStandards, value]) + + // Получаем данные выбранных эталонов из реального API const selectedStandardsData = selectedStandardIds - .map(id => getStandardById(id)) + .map(id => standards.find(s => s.id === id)) .filter(Boolean) return ( @@ -148,7 +75,7 @@ export const standardsDefinition: ElementDefinition =
{selectedStandardsData.length > 0 ? ( -
+
{selectedStandardsData.map( (standard, index) => standard && ( @@ -156,25 +83,27 @@ export const standardsDefinition: ElementDefinition = key={standard.id} className="flex items-center gap-2 rounded-md bg-muted/30 p-2 text-sm" > -
+
{index + 1}
- {standard.shortName} + {standard.name}
-
- {standard.accuracy} • {standard.range} +
+ + + {standard.registry_number} +
- - {getStandardTypeBadge(standard.type)} + {standard.range}
) @@ -195,12 +124,10 @@ export const standardsDefinition: ElementDefinition = value={selectedStandardIds} onChange={standardIds => { setSelectedStandardIds(standardIds) - // Преобразуем ID эталонов в их названия для сохранения в ячейки - const standardNames = standardIds.map( - id => getStandardById(id)?.name || '' - ) - onChange?.(standardNames) + // Сохраняем ID эталонов в value для записи в Excel + onChange?.(standardIds) }} + maxItems={config.maxItems} />
) @@ -213,8 +140,13 @@ export const standardsDefinition: ElementDefinition = // Используем targetCells из конфигурации const cells = cfg.targetCells || [] - // Сопоставляем значения с ячейками - const standardNames = Array.isArray(value) ? value : [] + // Конвертируем ID эталонов в их названия для записи в Excel + // Здесь используем моки для быстрого доступа к данным при записи в Excel + const standardIds = Array.isArray(value) ? value : [] + const standardNames = standardIds.map( + id => getStandardById(id)?.name || '' + ) + return cells.map((target, idx) => ({ target, value: standardNames[idx] || '', // Берем значение по индексу или пустую строку diff --git a/src/component/StandardsElement/hooks.ts b/src/component/StandardsElement/hooks.ts new file mode 100644 index 0000000..58244c2 --- /dev/null +++ b/src/component/StandardsElement/hooks.ts @@ -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}`) + }, + }) +} diff --git a/src/component/StandardsElement/mockStandards.ts b/src/component/StandardsElement/mockStandards.ts new file mode 100644 index 0000000..6bcd476 --- /dev/null +++ b/src/component/StandardsElement/mockStandards.ts @@ -0,0 +1,229 @@ +export interface MeasurementStandard { + id: string + name: string + shortName: string + type: string + registryNumber: string + range: string + accuracy: string + certificateNumber: string + validUntil: string +} + +// Общие моковые данные для эталонов +export 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', + }, + { + id: '6', + name: 'Эталон электрического напряжения 10 В', + shortName: 'ЭН-10В', + type: 'Электричество', + registryNumber: 'ГРСИ 12345-06', + range: '1-100 В', + accuracy: '±0.01 В', + certificateNumber: 'СИ-2024-006', + validUntil: '2025-08-15', + }, + { + id: '7', + name: 'Эталон силы тока 1 А', + shortName: 'ЭТ-1А', + type: 'Электричество', + registryNumber: 'ГРСИ 12345-07', + range: '0.1-10 А', + accuracy: '±0.001 А', + certificateNumber: 'СИ-2024-007', + validUntil: '2025-11-30', + }, + { + id: '8', + name: 'Эталон частоты 1 кГц', + shortName: 'ЭЧ-1кГц', + type: 'Частота', + registryNumber: 'ГРСИ 12345-08', + range: '10 Гц - 10 кГц', + accuracy: '±0.0001 Гц', + certificateNumber: 'СИ-2024-008', + validUntil: '2025-07-20', + }, + { + id: '9', + name: 'Эталон влажности 50% RH', + shortName: 'ЭВ-50%', + type: 'Влажность', + registryNumber: 'ГРСИ 12345-09', + range: '10-90% RH', + accuracy: '±0.5% RH', + certificateNumber: 'СИ-2024-009', + validUntil: '2025-04-10', + }, + { + id: '10', + name: 'Эталон освещенности 1000 лк', + shortName: 'ЭО-1000лк', + type: 'Освещенность', + registryNumber: 'ГРСИ 12345-10', + range: '1-10000 лк', + accuracy: '±1 лк', + certificateNumber: 'СИ-2024-010', + validUntil: '2025-10-05', + }, + { + id: '11', + name: 'Эталон скорости 10 м/с', + shortName: 'ЭС-10м/с', + type: 'Скорость', + registryNumber: 'ГРСИ 12345-11', + range: '0.1-50 м/с', + accuracy: '±0.01 м/с', + certificateNumber: 'СИ-2024-011', + validUntil: '2025-05-25', + }, + { + id: '12', + name: 'Эталон ускорения 9.8 м/с²', + shortName: 'ЭУ-9.8м/с²', + type: 'Ускорение', + registryNumber: 'ГРСИ 12345-12', + range: '0.1-20 м/с²', + accuracy: '±0.001 м/с²', + certificateNumber: 'СИ-2024-012', + validUntil: '2025-12-15', + }, + { + id: '13', + name: 'Эталон магнитной индукции 1 Тл', + shortName: 'ЭМИ-1Тл', + type: 'Магнетизм', + registryNumber: 'ГРСИ 12345-13', + range: '0.01-10 Тл', + accuracy: '±0.0001 Тл', + certificateNumber: 'СИ-2024-013', + validUntil: '2025-09-08', + }, + { + id: '14', + name: 'Эталон акустического давления 1 Па', + shortName: 'ЭАД-1Па', + type: 'Акустика', + registryNumber: 'ГРСИ 12345-14', + range: '0.02-200 Па', + accuracy: '±0.1 дБ', + certificateNumber: 'СИ-2024-014', + validUntil: '2025-06-12', + }, + { + id: '15', + name: 'Эталон угла поворота 1°', + shortName: 'ЭУП-1°', + type: 'Угол', + registryNumber: 'ГРСИ 12345-15', + range: '0-360°', + accuracy: '±0.001°', + certificateNumber: 'СИ-2024-015', + validUntil: '2025-08-30', + }, +] + +// Утилиты для работы с эталонами +export const getStandardById = ( + id: string +): MeasurementStandard | undefined => { + return mockStandards.find(standard => standard.id === id) +} + +export const getStandardByName = ( + name: string +): MeasurementStandard | undefined => { + return mockStandards.find(standard => standard.name === name) +} + +// Функция для получения полезного badge по типу эталона +export const getStandardTypeBadge = (type: string) => { + switch (type) { + case 'Масса': + return 'МАССА' + case 'Длина': + return 'ДЛИНА' + case 'Температура': + return 'ТЕМП' + case 'Давление': + return 'ДАВЛ' + case 'Время': + return 'ВРЕМЯ' + case 'Электричество': + return 'ЭЛЕКТ' + case 'Частота': + return 'ЧАСТ' + case 'Влажность': + return 'ВЛАЖ' + case 'Освещенность': + return 'СВЕТ' + case 'Скорость': + return 'СКОР' + case 'Ускорение': + return 'УСКОР' + case 'Магнетизм': + return 'МАГН' + case 'Акустика': + return 'ЗВУК' + case 'Угол': + return 'УГОЛ' + default: + return 'ЭТЛ' + } +} diff --git a/src/component/StandardsElement/types.ts b/src/component/StandardsElement/types.ts new file mode 100644 index 0000000..2b13527 --- /dev/null +++ b/src/component/StandardsElement/types.ts @@ -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[] +} diff --git a/src/page/StandardsManagementPage.tsx b/src/page/StandardsManagementPage.tsx new file mode 100644 index 0000000..8c493ee --- /dev/null +++ b/src/page/StandardsManagementPage.tsx @@ -0,0 +1,485 @@ +import { + useCreateStandard, + useDeleteStandard, + useStandards, + useUpdateStandard, +} from '@/component/StandardsElement/hooks' +import { + CreateStandardInput, + PatchStandardInput, + Standard, +} from '@/component/StandardsElement/types' +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 { Label } from '@/component/ui/label' +import { + Calendar, + Edit, + FileText, + Gauge, + Plus, + Search, + Trash2, + Weight, +} from 'lucide-react' +import { useEffect, useState } from 'react' + +export const StandardsManagementPage = () => { + const [searchTerm, setSearchTerm] = useState('') + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) + const [editingStandard, setEditingStandard] = useState(null) + const [deleteConfirmId, setDeleteConfirmId] = useState(null) + + const { data: standards = [], isLoading, error } = useStandards() + const createMutation = useCreateStandard() + const updateMutation = useUpdateStandard() + const deleteMutation = useDeleteStandard() + + // Фильтрация эталонов + const filteredStandards = standards.filter( + standard => + standard.name.toLowerCase().includes(searchTerm.toLowerCase()) || + standard.protocol_name.toLowerCase().includes(searchTerm.toLowerCase()) || + standard.registry_number.toLowerCase().includes(searchTerm.toLowerCase()) + ) + + const handleCreate = (input: CreateStandardInput) => { + createMutation.mutate(input, { + onSuccess: () => { + setIsCreateDialogOpen(false) + }, + }) + } + + const handleUpdate = (id: string, input: PatchStandardInput) => { + updateMutation.mutate( + { id, input }, + { + onSuccess: () => { + setEditingStandard(null) + }, + } + ) + } + + const handleDelete = (id: string) => { + deleteMutation.mutate(id, { + onSuccess: () => { + setDeleteConfirmId(null) + }, + }) + } + + if (isLoading) { + return ( +
+
+
+

Загрузка эталонов...

+
+
+ ) + } + + if (error) { + return ( +
+
+

Ошибка загрузки эталонов

+

{error.message}

+
+
+ ) + } + + return ( +
+ {/* Заголовок страницы */} +
+
+
+ +

Управление эталонами

+ + + Всего эталонов: {standards.length} + +
+ +
+
+ + {/* Основное содержимое */} +
+
+ {/* Поиск */} +
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+ + {/* Список эталонов */} + {filteredStandards.length > 0 ? ( +
+ {filteredStandards.map(standard => ( + setEditingStandard(standard)} + onDelete={() => setDeleteConfirmId(standard.id)} + /> + ))} +
+ ) : ( +
+
+ +

+ {searchTerm ? 'Эталоны не найдены' : 'Эталоны не созданы'} +

+

+ {searchTerm + ? `Попробуйте изменить поисковый запрос "${searchTerm}"` + : 'Создайте свой первый эталон для начала работы'} +

+
+
+ )} +
+
+ + {/* Диалоги */} + setIsCreateDialogOpen(false)} + onSave={handleCreate} + isLoading={createMutation.isPending} + title="Создать эталон" + /> + + setEditingStandard(null)} + onSave={input => + editingStandard && handleUpdate(editingStandard.id, input) + } + isLoading={updateMutation.isPending} + title="Редактировать эталон" + initialData={editingStandard || undefined} + /> + + setDeleteConfirmId(null)} + onConfirm={() => deleteConfirmId && handleDelete(deleteConfirmId)} + isLoading={deleteMutation.isPending} + standardName={standards.find(s => s.id === deleteConfirmId)?.name || ''} + /> +
+ ) +} + +// Компонент карточки эталона +interface StandardCardProps { + standard: Standard + onEdit: () => void + onDelete: () => void +} + +const StandardCard = ({ standard, onEdit, onDelete }: StandardCardProps) => { + const isExpiringSoon = () => { + const expiryDate = new Date(standard.valid_until) + const now = new Date() + const monthsUntilExpiry = + (expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24 * 30) + return monthsUntilExpiry <= 3 + } + + return ( +
+ {/* Основная информация */} +
+
+
+ {standard.protocol_name} +
+
+ {standard.name} +
+ {isExpiringSoon() && ( + + + Истекает + + )} +
+
+ + {/* Дополнительная информация */} +
+
+ + {standard.registry_number} +
+ + {standard.range} + +
+ + {standard.accuracy} +
+
+ + {/* Действия */} +
+ + +
+
+ ) +} + +// Диалог создания/редактирования эталона +interface StandardDialogProps { + isOpen: boolean + onClose: () => void + onSave: (input: CreateStandardInput) => void + isLoading: boolean + title: string + initialData?: Standard +} + +const StandardDialog = ({ + isOpen, + onClose, + onSave, + isLoading, + title, + initialData, +}: StandardDialogProps) => { + const [formData, setFormData] = useState({ + name: initialData?.name || '', + protocol_name: initialData?.protocol_name || '', + registry_number: initialData?.registry_number || '', + range: initialData?.range || '', + accuracy: initialData?.accuracy || '', + valid_until: initialData?.valid_until || '', + }) + + // Обновляем formData при изменении initialData + useEffect(() => { + if (initialData) { + setFormData({ + name: initialData.name || '', + protocol_name: initialData.protocol_name || '', + registry_number: initialData.registry_number || '', + range: initialData.range || '', + accuracy: initialData.accuracy || '', + valid_until: initialData.valid_until || '', + }) + } + }, [initialData]) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + onSave(formData) + } + + const handleClose = () => { + if (!initialData) { + setFormData({ + name: '', + protocol_name: '', + registry_number: '', + range: '', + accuracy: '', + valid_until: '', + }) + } + onClose() + } + + return ( + + + + {title} + + +
+
+ + + setFormData(prev => ({ ...prev, name: e.target.value })) + } + placeholder="Эталон массы 1 кг" + required + /> +
+ +
+ + + setFormData(prev => ({ + ...prev, + protocol_name: e.target.value, + })) + } + placeholder="ЭМ-1кг" + required + /> +
+ +
+ + + setFormData(prev => ({ + ...prev, + registry_number: e.target.value, + })) + } + placeholder="ГРСИ 12345-01" + required + /> +
+ +
+ + + setFormData(prev => ({ ...prev, range: e.target.value })) + } + placeholder="0.5-2 кг" + required + /> +
+ +
+ + + setFormData(prev => ({ ...prev, accuracy: e.target.value })) + } + placeholder="±0.001 г" + required + /> +
+ +
+ + + setFormData(prev => ({ ...prev, valid_until: e.target.value })) + } + required + /> +
+ +
+ + +
+
+
+
+ ) +} + +// Диалог подтверждения удаления +interface DeleteConfirmDialogProps { + isOpen: boolean + onClose: () => void + onConfirm: () => void + isLoading: boolean + standardName: string +} + +const DeleteConfirmDialog = ({ + isOpen, + onClose, + onConfirm, + isLoading, + standardName, +}: DeleteConfirmDialogProps) => { + return ( + + + + Удалить эталон? + + +
+

+ Вы действительно хотите удалить эталон{' '} + {standardName}? +

+

+ Это действие нельзя отменить. +

+ +
+ + +
+
+
+
+ ) +} diff --git a/vite.config.ts b/vite.config.ts index 3b753ed..2b95a5f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -18,7 +18,8 @@ export default defineConfig({ port: 80, // Явно указать порт (по умолчанию 5173) proxy: { '/api': { - target: 'http://192.169.0.163:8000', + //192.168.2.66 + target: 'http://localhost:8000', changeOrigin: true, rewrite: path => path.replace(/^\/api/, ''), },