240 lines
7.8 KiB
TypeScript
240 lines
7.8 KiB
TypeScript
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>
|
||
)
|
||
}
|