рефакторинг
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Input } from '@/component/ui/input'
|
||||
import { ElementDefinition } from '@/lib/element-registry'
|
||||
import { ElementOption } from '@/type/template'
|
||||
import { Plus, Square, Trash2 } from 'lucide-react'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
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 { Label } from '@/component/ui/label'
|
||||
import { ElementDefinition } from '@/lib/element-registry'
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -91,7 +91,7 @@ const formatLastUpdated = (dateString: string) => {
|
||||
const monthShortRu = date
|
||||
.toLocaleString('ru-RU', { month: 'short' })
|
||||
.replace(/\./, '')
|
||||
.replace(/^./, (s) => s.toUpperCase())
|
||||
.replace(/^./, s => s.toUpperCase())
|
||||
const day = date.getDate()
|
||||
const time: string = date.toLocaleString('ru-RU', {
|
||||
hour: '2-digit',
|
||||
@@ -103,8 +103,8 @@ const formatLastUpdated = (dateString: string) => {
|
||||
} else if (date.toDateString() === yesterday.toDateString()) {
|
||||
return `Вчера в ${time}`
|
||||
} else {
|
||||
return `${weekdayRu.replace(/^./, (s) =>
|
||||
s.toUpperCase(),
|
||||
return `${weekdayRu.replace(/^./, s =>
|
||||
s.toUpperCase()
|
||||
)} ${day} ${monthShortRu} ${time}`
|
||||
}
|
||||
}
|
||||
@@ -136,10 +136,10 @@ function CalibrationCondInfo({
|
||||
'frequency',
|
||||
]
|
||||
|
||||
fields.forEach((field) => {
|
||||
fields.forEach(field => {
|
||||
const validation = validateField(
|
||||
field,
|
||||
tempConditions[field as keyof CalibrationConditions] as string,
|
||||
tempConditions[field as keyof CalibrationConditions] as string
|
||||
)
|
||||
if (!validation.isValid && validation.message) {
|
||||
errors[field] = validation.message
|
||||
@@ -151,11 +151,11 @@ function CalibrationCondInfo({
|
||||
}
|
||||
|
||||
const handleFieldChange = (field: string, value: string) => {
|
||||
setTempConditions((prev) => ({ ...prev, [field]: value }))
|
||||
setTempConditions(prev => ({ ...prev, [field]: value }))
|
||||
|
||||
// Валидация в реальном времени
|
||||
const validation = validateField(field, value)
|
||||
setValidationErrors((prev) => ({
|
||||
setValidationErrors(prev => ({
|
||||
...prev,
|
||||
[field]: validation.isValid ? '' : validation.message || '',
|
||||
}))
|
||||
@@ -187,7 +187,7 @@ function CalibrationCondInfo({
|
||||
// Устанавливаем фокус на первое поле через небольшую задержку
|
||||
setTimeout(() => {
|
||||
const firstInput = document.getElementById(
|
||||
'modal-temperature',
|
||||
'modal-temperature'
|
||||
) as HTMLInputElement
|
||||
if (firstInput) {
|
||||
firstInput.focus()
|
||||
@@ -199,7 +199,7 @@ function CalibrationCondInfo({
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (
|
||||
e.key === 'Enter' &&
|
||||
!Object.keys(validationErrors).some((key) => validationErrors[key])
|
||||
!Object.keys(validationErrors).some(key => validationErrors[key])
|
||||
) {
|
||||
handleSave()
|
||||
}
|
||||
@@ -321,10 +321,10 @@ function CalibrationCondInfo({
|
||||
key as keyof CalibrationConditions
|
||||
] as string
|
||||
}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
handleFieldChange(key, e.target.value)
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleSave()
|
||||
@@ -334,8 +334,8 @@ function CalibrationCondInfo({
|
||||
hasError
|
||||
? 'border-destructive focus-visible:ring-destructive'
|
||||
: isValid
|
||||
? 'border-green-500 focus-visible:ring-green-500'
|
||||
: ''
|
||||
? 'border-green-500 focus-visible:ring-green-500'
|
||||
: ''
|
||||
}`}
|
||||
placeholder={placeholder}
|
||||
type="number"
|
||||
@@ -361,7 +361,7 @@ function CalibrationCondInfo({
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -385,7 +385,7 @@ function CalibrationCondInfo({
|
||||
disabled={
|
||||
isLoading ||
|
||||
Object.keys(validationErrors).some(
|
||||
(key) => validationErrors[key],
|
||||
key => validationErrors[key]
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -463,7 +463,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.temperature || '20'}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
@@ -489,7 +489,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.humidity || '65'}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
@@ -515,7 +515,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.pressure || '101.3'}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
@@ -541,7 +541,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.voltage || '220'}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
@@ -567,7 +567,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
|
||||
<Input
|
||||
type="number"
|
||||
value={config.defaultConditions?.frequency || '50'}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...config,
|
||||
defaultConditions: {
|
||||
@@ -632,7 +632,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
|
||||
<CalibrationCondInfo
|
||||
conditions={conditions}
|
||||
isOutdated={isOutdated}
|
||||
onUpdate={async (newConditions) => {
|
||||
onUpdate={async newConditions => {
|
||||
onChange?.(newConditions)
|
||||
return true
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Input } from '@/component/ui/input'
|
||||
import { ElementDefinition } from '@/lib/element-registry'
|
||||
import { Calendar } from 'lucide-react'
|
||||
|
||||
@@ -22,7 +22,7 @@ export const dateDefinition: ElementDefinition<DateConfig, string> = {
|
||||
type="checkbox"
|
||||
id="required"
|
||||
checked={config.required}
|
||||
onChange={(e) => onChange({ ...config, required: e.target.checked })}
|
||||
onChange={e => onChange({ ...config, required: e.target.checked })}
|
||||
/>
|
||||
<label htmlFor="required" className="text-sm font-medium">
|
||||
Обязательное поле
|
||||
@@ -42,7 +42,7 @@ export const dateDefinition: ElementDefinition<DateConfig, string> = {
|
||||
<Input
|
||||
type="date"
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
onChange={e => onChange?.(e.target.value)}
|
||||
required={config.required}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Input } from '@/component/ui/input'
|
||||
import { ElementDefinition } from '@/lib/element-registry'
|
||||
import { Hash } from 'lucide-react'
|
||||
|
||||
@@ -40,7 +40,7 @@ export const numberDefinition: ElementDefinition<NumberConfig, number> = {
|
||||
type="number"
|
||||
placeholder={config.placeholder || '0'}
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange?.(parseFloat(e.target.value) || 0)}
|
||||
onChange={e => onChange?.(parseFloat(e.target.value) || 0)}
|
||||
required={config.required}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Input } from '@/component/ui/input'
|
||||
import { ElementDefinition } from '@/lib/element-registry'
|
||||
import { ElementOption } from '@/type/template'
|
||||
import { CheckSquare, Plus, Trash2 } from 'lucide-react'
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Input } from '@/component/ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
} from '@/component/ui/select'
|
||||
import { ElementDefinition } from '@/lib/element-registry'
|
||||
import { ElementOption } from '@/type/template'
|
||||
import { ChevronDown, Plus, Trash2 } from 'lucide-react'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Input } from '@/component/ui/input'
|
||||
import { ElementDefinition } from '@/lib/element-registry'
|
||||
import { Type } from 'lucide-react'
|
||||
|
||||
@@ -38,7 +38,7 @@ export const textDefinition: ElementDefinition<TextConfig, string> = {
|
||||
<Input
|
||||
placeholder={config.placeholder || 'Введите текст'}
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
onChange={e => onChange?.(e.target.value)}
|
||||
required={config.required}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Textarea } from '@/component/ui/textarea'
|
||||
import { ElementDefinition } from '@/lib/element-registry'
|
||||
import { FileText } from 'lucide-react'
|
||||
|
||||
@@ -39,7 +39,7 @@ export const textareaDefinition: ElementDefinition<TextareaConfig, string> = {
|
||||
<Textarea
|
||||
placeholder={config.placeholder || 'Введите текст'}
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
onChange={e => onChange?.(e.target.value)}
|
||||
required={config.required}
|
||||
rows={3}
|
||||
className="w-full resize-none"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Badge } from '@/component/ui/badge'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card'
|
||||
import { Separator } from '@/component/ui/separator'
|
||||
import { Calendar, Check, FileText, Settings } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
@@ -41,15 +41,15 @@ export function StandardsDialog({
|
||||
registryNumber,
|
||||
}: StandardsDialogProps) {
|
||||
const [selectedStandards, setSelectedStandards] = useState<string[]>(
|
||||
config.selectedStandards,
|
||||
config.selectedStandards
|
||||
)
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const toggleStandard = (standardId: string) => {
|
||||
setSelectedStandards((prev) => {
|
||||
setSelectedStandards(prev => {
|
||||
if (prev.includes(standardId)) {
|
||||
return prev.filter((id) => id !== standardId)
|
||||
return prev.filter(id => id !== standardId)
|
||||
} else if (prev.length < 7) {
|
||||
return [...prev, standardId]
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export function StandardsDialog({
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<div className="scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-600 scrollbar-track-transparent grid max-h-96 grid-cols-1 gap-3 overflow-y-auto md:grid-cols-2">
|
||||
{standards.map((standard) => {
|
||||
{standards.map(standard => {
|
||||
const isSelected = selectedStandards.includes(standard.id)
|
||||
const isExpiring = isExpiringSoon(standard.validUntil)
|
||||
|
||||
@@ -155,7 +155,7 @@ export function StandardsDialog({
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Действует до:{' '}
|
||||
{new Date(standard.validUntil).toLocaleDateString(
|
||||
'ru-RU',
|
||||
'ru-RU'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Input } from '@/component/ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
} from '@/component/ui/select'
|
||||
import { CellTarget } from '@/type/template'
|
||||
import { Calendar, Settings, Trash2 } from 'lucide-react'
|
||||
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar, Settings } from "lucide-react";
|
||||
import { Badge } from '@/component/ui/badge'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Calendar, Settings } from 'lucide-react'
|
||||
|
||||
interface StandardsConfig {
|
||||
registryNumber: string;
|
||||
sheet: string;
|
||||
startCell: string;
|
||||
maxItems: number;
|
||||
targetCells: any[];
|
||||
registryNumber: string
|
||||
sheet: string
|
||||
startCell: string
|
||||
maxItems: number
|
||||
targetCells: any[]
|
||||
}
|
||||
|
||||
interface StandardsPreviewProps {
|
||||
config: StandardsConfig;
|
||||
config: StandardsConfig
|
||||
}
|
||||
|
||||
export const StandardsPreview: React.FC<StandardsPreviewProps> = ({ config }) => {
|
||||
export const StandardsPreview: React.FC<StandardsPreviewProps> = ({
|
||||
config,
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
|
||||
<div className="p-4 border border-gray-200 rounded-lg bg-white">
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-4 w-4 text-gray-500" />
|
||||
<span className="text-sm font-medium">Измерительные эталоны</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-gray-500">
|
||||
ГРСИ: {config.registryNumber || "Не указан"}
|
||||
ГРСИ: {config.registryNumber || 'Не указан'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Максимум: {config.maxItems} эталонов
|
||||
@@ -36,7 +38,7 @@ export const StandardsPreview: React.FC<StandardsPreviewProps> = ({ config }) =>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" disabled className="text-xs">
|
||||
<Calendar className="h-3 w-3 mr-1" />
|
||||
<Calendar className="mr-1 h-3 w-3" />
|
||||
Выбрать эталоны
|
||||
</Button>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
@@ -45,11 +47,11 @@ export const StandardsPreview: React.FC<StandardsPreviewProps> = ({ config }) =>
|
||||
</div>
|
||||
|
||||
{config.targetCells && config.targetCells.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{config.targetCells.map((target, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block px-1.5 py-0.5 bg-blue-100 text-blue-700 text-xs rounded"
|
||||
className="inline-block rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700"
|
||||
title={target.displayName}
|
||||
>
|
||||
{target.sheet}!{target.cell}
|
||||
@@ -60,5 +62,5 @@ export const StandardsPreview: React.FC<StandardsPreviewProps> = ({ config }) =>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,214 +1,227 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Calendar, Check, FileText, Settings, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
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;
|
||||
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;
|
||||
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: '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: '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: '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: '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: '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"
|
||||
registryNumber = 'ГРСИ 12345',
|
||||
}: StandardsSelectorProps) {
|
||||
const [selectedStandards, setSelectedStandards] = useState<string[]>(value);
|
||||
const [selectedStandards, setSelectedStandards] = useState<string[]>(value)
|
||||
|
||||
const toggleStandard = (standardId: string) => {
|
||||
setSelectedStandards(prev => {
|
||||
if (prev.includes(standardId)) {
|
||||
return prev.filter(id => id !== standardId);
|
||||
return prev.filter(id => id !== standardId)
|
||||
} else if (prev.length < 7) {
|
||||
return [...prev, standardId];
|
||||
return [...prev, standardId]
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
return prev
|
||||
})
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onChange(selectedStandards);
|
||||
onClose();
|
||||
};
|
||||
onChange(selectedStandards)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setSelectedStandards(value); // Сбрасываем к исходному значению
|
||||
onClose();
|
||||
};
|
||||
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;
|
||||
};
|
||||
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-w-4xl max-h-[90vh] overflow-hidden">
|
||||
<DialogContent className="max-h-[90vh] max-w-4xl overflow-hidden">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<DialogTitle className="text-lg flex items-center gap-2">
|
||||
<Settings className="w-5 h-5" />
|
||||
<DialogTitle className="flex items-center gap-2 text-lg">
|
||||
<Settings className="h-5 w-5" />
|
||||
Измерительные эталоны
|
||||
</DialogTitle>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
ГРСИ: {registryNumber} • Выбрано: {selectedStandards.length}/7
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={handleClose}>
|
||||
<X className="w-4 h-4" />
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-96 overflow-y-auto">
|
||||
{mockStandards.map((standard) => {
|
||||
const isSelected = selectedStandards.includes(standard.id);
|
||||
const isExpiring = isExpiringSoon(standard.validUntil);
|
||||
|
||||
<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={`p-3 rounded-lg border cursor-pointer transition-all duration-200 ${
|
||||
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"
|
||||
? '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 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-colors ${
|
||||
isSelected
|
||||
? "border-primary bg-primary"
|
||||
: "border-border"
|
||||
}`}>
|
||||
{isSelected && <Check className="w-3 h-3 text-primary-foreground" />}
|
||||
<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="flex-1 min-w-0">
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm text-foreground leading-tight">
|
||||
<div className="text-sm font-medium leading-tight text-foreground">
|
||||
{standard.shortName}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
||||
<div className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||||
{standard.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 shrink-0">
|
||||
|
||||
<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="w-3 h-3 mr-1" />
|
||||
<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="w-3 h-3" />
|
||||
<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')}
|
||||
Действует до:{' '}
|
||||
{new Date(standard.validUntil).toLocaleDateString(
|
||||
'ru-RU'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-4 border-t">
|
||||
|
||||
<div className="flex items-center justify-between border-t pt-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Максимум 7 эталонов. Выбрано: {selectedStandards.length}
|
||||
</div>
|
||||
@@ -216,13 +229,11 @@ export function StandardsSelector({
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
Сохранить
|
||||
</Button>
|
||||
<Button onClick={handleSave}>Сохранить</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { FileText, Settings, Wrench } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Template } from '../../type/template'
|
||||
import { Button } from '../ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'
|
||||
import { Template } from '../type/template'
|
||||
import { Button } from './ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card'
|
||||
|
||||
interface TemplateCardProps {
|
||||
template: Template
|
||||
onDelete: (template: Template) => void
|
||||
isSelected?: boolean
|
||||
}
|
||||
|
||||
const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
template,
|
||||
onDelete,
|
||||
isSelected = false,
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { FileText, Upload } from 'lucide-react'
|
||||
import React, { useRef } from 'react'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Separator } from '@/component/ui/separator'
|
||||
import {
|
||||
AlignCenter,
|
||||
AlignLeft,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Template } from '@/type/template'
|
||||
import { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DualSpreadsheet from '@/components/DualSpreadsheet/DualSpreadsheet'
|
||||
import DualSpreadsheet from '@/component/DualSpreadsheet/DualSpreadsheet'
|
||||
import { ExcelParseResult } from '@/service/fileApiSevice'
|
||||
import React from 'react'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTemplateContext } from '@/context/TemplateContext'
|
||||
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
||||
import {
|
||||
getFileData,
|
||||
getLatestFileForTemplate,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
||||
import { Template } from '@/type/template'
|
||||
import { Circle, CircleCheck, FileText, Plus, Trash2 } from 'lucide-react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTemplateContext } from '../../context/TemplateContext'
|
||||
import { Template } from '../../type/template'
|
||||
import TemplateCard from '../TemplateCard'
|
||||
import { Button } from '../ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
@@ -13,16 +14,14 @@ import {
|
||||
DialogTrigger,
|
||||
} from '../ui/dialog'
|
||||
import { Input } from '../ui/input'
|
||||
import TemplateCard from './TemplateCard'
|
||||
import { TemplateEditor } from './TemplateEditor'
|
||||
|
||||
interface TemplateManagerProps {
|
||||
onTemplateSelect?: (template: Template) => void
|
||||
templateId?: string
|
||||
template?: Template
|
||||
}
|
||||
|
||||
export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
templateId,
|
||||
template: initialTemplate,
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const {
|
||||
@@ -32,7 +31,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
addTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate,
|
||||
loadTemplates,
|
||||
refetchTemplates,
|
||||
} = useTemplateContext()
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
|
||||
null
|
||||
@@ -50,14 +49,11 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
|
||||
// Автоматически открываем редактирование шаблона, если передан templateId
|
||||
useEffect(() => {
|
||||
if (templateId) {
|
||||
const template = templates.find(t => t.id === templateId)
|
||||
if (template) {
|
||||
setSelectedTemplate(template)
|
||||
setIsEditMode(true)
|
||||
}
|
||||
if (initialTemplate) {
|
||||
setSelectedTemplate(initialTemplate)
|
||||
setIsEditMode(true)
|
||||
}
|
||||
}, [templateId, templates])
|
||||
}, [initialTemplate])
|
||||
|
||||
const handleCreateTemplate = async () => {
|
||||
if (!newTemplateName.trim()) return
|
||||
@@ -70,58 +66,44 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
elements: [],
|
||||
layoutSettings: undefined,
|
||||
})
|
||||
|
||||
// Находим созданный шаблон и открываем его для редактирования
|
||||
// Поскольку addTemplate может занять время, используем небольшую задержку
|
||||
setTimeout(() => {
|
||||
const newTemplate = templates.find(
|
||||
t => t.name === newTemplateName.trim()
|
||||
)
|
||||
if (newTemplate) {
|
||||
setSelectedTemplate(newTemplate)
|
||||
setIsEditMode(true)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
setIsCreateDialogOpen(false)
|
||||
setNewTemplateName('')
|
||||
setNewTemplateDescription('')
|
||||
} catch (error) {
|
||||
console.error('Ошибка при создании шаблона:', error)
|
||||
// Здесь можно добавить уведомление пользователю об ошибке
|
||||
await refetchTemplates()
|
||||
const created = templates.find(t => t.name === newTemplateName.trim())
|
||||
if (created) {
|
||||
setSelectedTemplate(created)
|
||||
setIsEditMode(true)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error creating template:', err)
|
||||
alert('Ошибка при создании шаблона. Попробуйте еще раз.')
|
||||
} finally {
|
||||
setIsCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTemplateUpdate = async (updatedTemplate: Template) => {
|
||||
try {
|
||||
await updateTemplate(updatedTemplate)
|
||||
setSelectedTemplate(updatedTemplate)
|
||||
} catch (error) {
|
||||
console.error('Ошибка при обновлении шаблона:', error)
|
||||
alert('Ошибка при обновлении шаблона. Попробуйте еще раз.')
|
||||
}
|
||||
}
|
||||
// const handleTemplateUpdate = async (updatedTemplate: Template) => {
|
||||
// try {
|
||||
// await updateTemplate(updatedTemplate)
|
||||
// setSelectedTemplate(updatedTemplate)
|
||||
// } catch (err) {
|
||||
// console.error('Error updating template:', err)
|
||||
// alert('Ошибка при обновлении шаблона. Попробуйте еще раз.')
|
||||
// }
|
||||
// }
|
||||
|
||||
const handleDeleteTemplates = () => {
|
||||
templatesToDelete.forEach(template => {
|
||||
deleteTemplate(template.id)
|
||||
})
|
||||
templatesToDelete.forEach(t => deleteTemplate(t.id))
|
||||
setTemplatesToDelete([])
|
||||
setIsSelectionMode(false)
|
||||
setSelectedTemplates(new Set())
|
||||
}
|
||||
|
||||
const toggleTemplateSelection = (templateId: string) => {
|
||||
const newSelected = new Set(selectedTemplates)
|
||||
if (newSelected.has(templateId)) {
|
||||
newSelected.delete(templateId)
|
||||
} else {
|
||||
newSelected.add(templateId)
|
||||
}
|
||||
setSelectedTemplates(newSelected)
|
||||
const toggleTemplateSelection = (id: UUID) => {
|
||||
const next = new Set(selectedTemplates)
|
||||
next.has(id) ? next.delete(id) : next.add(id)
|
||||
setSelectedTemplates(next)
|
||||
}
|
||||
|
||||
const handleSelectionModeToggle = () => {
|
||||
@@ -135,14 +117,12 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
}
|
||||
|
||||
const handleDeleteSelected = () => {
|
||||
const selectedTemplatesList = templates.filter(t =>
|
||||
selectedTemplates.has(t.id)
|
||||
)
|
||||
setTemplatesToDelete(selectedTemplatesList)
|
||||
const toDelete = templates.filter(t => selectedTemplates.has(t.id))
|
||||
setTemplatesToDelete(toDelete)
|
||||
}
|
||||
|
||||
const handleRetry = () => {
|
||||
loadTemplates()
|
||||
refetchTemplates()
|
||||
}
|
||||
|
||||
if (isEditMode && selectedTemplate) {
|
||||
@@ -152,9 +132,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
onBack={() => {
|
||||
setIsEditMode(false)
|
||||
setSelectedTemplate(null)
|
||||
if (templateId) {
|
||||
navigate('/templates')
|
||||
}
|
||||
navigate('/templates')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -169,7 +147,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
<h3 className="mb-2 text-lg font-medium text-gray-900">
|
||||
Ошибка загрузки шаблонов
|
||||
</h3>
|
||||
<p className="mb-4 text-gray-600">{error}</p>
|
||||
<p className="mb-4 text-gray-600">{error.message}</p>
|
||||
<Button onClick={handleRetry}>Попробовать снова</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user