Files
protoc-frontend/src/component/StandardsElement/StandardsSelector.tsx
2025-07-21 17:07:03 +03:00

240 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Badge } from '@/component/ui/badge'
import { Button } from '@/component/ui/button'
import {
Dialog,
DialogContent,
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
}
interface StandardsSelectorProps {
isOpen: boolean
onClose: () => void
value: string[]
onChange: (value: string[]) => void
registryNumber?: string
}
// Моковые данные для эталонов
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',
}: StandardsSelectorProps) {
const [selectedStandards, setSelectedStandards] = useState<string[]>(value)
const toggleStandard = (standardId: string) => {
setSelectedStandards(prev => {
if (prev.includes(standardId)) {
return prev.filter(id => id !== standardId)
} else if (prev.length < 7) {
return [...prev, standardId]
}
return prev
})
}
const handleSave = () => {
onChange(selectedStandards)
onClose()
}
const handleClose = () => {
setSelectedStandards(value) // Сбрасываем к исходному значению
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
}
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-h-[90vh] max-w-4xl overflow-hidden">
<DialogHeader>
<div className="flex items-center justify-between">
<div>
<DialogTitle className="flex items-center gap-2 text-lg">
<Settings className="h-5 w-5" />
Измерительные эталоны
</DialogTitle>
<p className="mt-1 text-sm text-muted-foreground">
ГРСИ: {registryNumber} Выбрано: {selectedStandards.length}/7
</p>
</div>
<Button variant="ghost" size="sm" onClick={handleClose}>
<X className="h-4 w-4" />
</Button>
</div>
</DialogHeader>
<div className="space-y-4">
<div className="grid max-h-96 grid-cols-1 gap-3 overflow-y-auto md:grid-cols-2">
{mockStandards.map(standard => {
const isSelected = selectedStandards.includes(standard.id)
const isExpiring = isExpiringSoon(standard.validUntil)
return (
<div
key={standard.id}
onClick={() => 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'
}`}
>
<div className="flex items-start gap-3">
<div
className={`mt-1 flex h-5 w-5 items-center justify-center rounded-full border-2 transition-colors ${
isSelected
? 'border-primary bg-primary'
: 'border-border'
}`}
>
{isSelected && (
<Check className="h-3 w-3 text-primary-foreground" />
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<div className="text-sm font-medium leading-tight text-foreground">
{standard.shortName}
</div>
<div className="mt-1 line-clamp-2 text-xs text-muted-foreground">
{standard.name}
</div>
</div>
<div className="flex shrink-0 flex-col gap-1">
<Badge variant="secondary" className="text-xs">
{standard.accuracy}
</Badge>
{isExpiring && (
<Badge variant="destructive" className="text-xs">
<Calendar className="mr-1 h-3 w-3" />
Истекает
</Badge>
)}
</div>
</div>
<div className="mt-2 space-y-1">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<FileText className="h-3 w-3" />
{standard.registryNumber}
</div>
<div className="text-xs text-muted-foreground">
Диапазон: {standard.range}
</div>
<div className="text-xs text-muted-foreground">
Действует до:{' '}
{new Date(standard.validUntil).toLocaleDateString(
'ru-RU'
)}
</div>
</div>
</div>
</div>
</div>
)
})}
</div>
<div className="flex items-center justify-between border-t pt-4">
<div className="text-sm text-muted-foreground">
Максимум 7 эталонов. Выбрано: {selectedStandards.length}
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleClose}>
Отмена
</Button>
<Button onClick={handleSave}>Сохранить</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
)
}