рефакторинг

This commit is contained in:
2025-07-21 17:07:03 +03:00
parent 84f0c9c6aa
commit 5737e37386
35 changed files with 526 additions and 579 deletions

View File

@@ -31,10 +31,10 @@ module.exports = {
pathGroups: [ pathGroups: [
{ pattern: 'react', group: 'builtin' }, { pattern: 'react', group: 'builtin' },
{ pattern: '~shared/**', group: 'internal', position: 'before' }, { pattern: '~shared/**', group: 'internal', position: 'before' },
{ pattern: '~entities/**', group: 'internal', position: 'before' }, { pattern: '~entitiy/**', group: 'internal', position: 'before' },
{ pattern: '~features/**', group: 'internal', position: 'before' }, { pattern: '~feature/**', group: 'internal', position: 'before' },
{ pattern: '~widgets/**', group: 'internal', position: 'before' }, { pattern: '~widget/**', group: 'internal', position: 'before' },
{ pattern: '~pages/**', group: 'internal', position: 'before' }, { pattern: '~page/**', group: 'internal', position: 'before' },
], ],
pathGroupsExcludedImportTypes: ['builtin'], pathGroupsExcludedImportTypes: ['builtin'],
groups: [ groups: [

1
shared/types.ts Normal file
View File

@@ -0,0 +1 @@
type UUID = string

View File

@@ -2,34 +2,39 @@ import { FC } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { TemplateProvider } from '../context/TemplateContext' import { TemplateProvider } from '@/entitiy/template/model/TemplateContext'
import { ElementsCreation } from '../page/ElementsCreation' import { ElementsCreation } from '@/page/ElementsCreation'
import { ProtocolCreation } from '../page/ProtocolCreation' import { ProtocolCreation } from '@/page/ProtocolCreation'
import { TemplateSetup } from '../page/TemplateSetup' import { TemplateEditPage } from '@/page/TemplateEditPage'
import { TemplatesOverviewPage } from '@/page/TemplatesOverviewPage'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Layout } from './Layout' import { Layout } from './Layout'
const queryClient = new QueryClient()
const App: FC = () => { const App: FC = () => {
return ( return (
<TemplateProvider> <QueryClientProvider client={queryClient}>
<Routes> <TemplateProvider>
<Route path="/" element={<Layout />}> <Routes>
<Route index element={<Navigate to="/templates" replace />} /> <Route path="/" element={<Layout />}>
<Route path="templates/" element={<TemplateSetup />} /> <Route index element={<Navigate to="/templates" replace />} />
<Route <Route path="templates/" element={<TemplatesOverviewPage />} />
path="templates/:templateId/edit" <Route
element={<TemplateSetup />} path="templates/:templateId/edit"
/> element={<TemplateEditPage />}
<Route />
path="templates/:templateId/elements" <Route
element={<ElementsCreation />} path="templates/:templateId/elements"
/> element={<ElementsCreation />}
<Route />
path="templates/:templateId/protocols" <Route
element={<ProtocolCreation />} path="templates/:templateId/protocols"
/> element={<ProtocolCreation />}
</Route> />
</Routes> </Route>
</TemplateProvider> </Routes>
</TemplateProvider>
</QueryClientProvider>
) )
} }

View File

@@ -1,5 +1,5 @@
import { Button } from '@/components/ui/button' import { Button } from '@/component/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/component/ui/input'
import { ElementDefinition } from '@/lib/element-registry' import { ElementDefinition } from '@/lib/element-registry'
import { ElementOption } from '@/type/template' import { ElementOption } from '@/type/template'
import { Plus, Square, Trash2 } from 'lucide-react' import { Plus, Square, Trash2 } from 'lucide-react'

View File

@@ -1,8 +1,8 @@
import { Badge } from '@/components/ui/badge' import { Badge } from '@/component/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/component/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card'
import { Input } from '@/components/ui/input' import { Input } from '@/component/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/component/ui/label'
import { ElementDefinition } from '@/lib/element-registry' import { ElementDefinition } from '@/lib/element-registry'
import { import {
AlertCircle, AlertCircle,
@@ -91,7 +91,7 @@ const formatLastUpdated = (dateString: string) => {
const monthShortRu = date const monthShortRu = date
.toLocaleString('ru-RU', { month: 'short' }) .toLocaleString('ru-RU', { month: 'short' })
.replace(/\./, '') .replace(/\./, '')
.replace(/^./, (s) => s.toUpperCase()) .replace(/^./, s => s.toUpperCase())
const day = date.getDate() const day = date.getDate()
const time: string = date.toLocaleString('ru-RU', { const time: string = date.toLocaleString('ru-RU', {
hour: '2-digit', hour: '2-digit',
@@ -103,8 +103,8 @@ const formatLastUpdated = (dateString: string) => {
} else if (date.toDateString() === yesterday.toDateString()) { } else if (date.toDateString() === yesterday.toDateString()) {
return `Вчера в ${time}` return `Вчера в ${time}`
} else { } else {
return `${weekdayRu.replace(/^./, (s) => return `${weekdayRu.replace(/^./, s =>
s.toUpperCase(), s.toUpperCase()
)} ${day} ${monthShortRu} ${time}` )} ${day} ${monthShortRu} ${time}`
} }
} }
@@ -136,10 +136,10 @@ function CalibrationCondInfo({
'frequency', 'frequency',
] ]
fields.forEach((field) => { fields.forEach(field => {
const validation = validateField( const validation = validateField(
field, field,
tempConditions[field as keyof CalibrationConditions] as string, tempConditions[field as keyof CalibrationConditions] as string
) )
if (!validation.isValid && validation.message) { if (!validation.isValid && validation.message) {
errors[field] = validation.message errors[field] = validation.message
@@ -151,11 +151,11 @@ function CalibrationCondInfo({
} }
const handleFieldChange = (field: string, value: string) => { const handleFieldChange = (field: string, value: string) => {
setTempConditions((prev) => ({ ...prev, [field]: value })) setTempConditions(prev => ({ ...prev, [field]: value }))
// Валидация в реальном времени // Валидация в реальном времени
const validation = validateField(field, value) const validation = validateField(field, value)
setValidationErrors((prev) => ({ setValidationErrors(prev => ({
...prev, ...prev,
[field]: validation.isValid ? '' : validation.message || '', [field]: validation.isValid ? '' : validation.message || '',
})) }))
@@ -187,7 +187,7 @@ function CalibrationCondInfo({
// Устанавливаем фокус на первое поле через небольшую задержку // Устанавливаем фокус на первое поле через небольшую задержку
setTimeout(() => { setTimeout(() => {
const firstInput = document.getElementById( const firstInput = document.getElementById(
'modal-temperature', 'modal-temperature'
) as HTMLInputElement ) as HTMLInputElement
if (firstInput) { if (firstInput) {
firstInput.focus() firstInput.focus()
@@ -199,7 +199,7 @@ function CalibrationCondInfo({
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {
if ( if (
e.key === 'Enter' && e.key === 'Enter' &&
!Object.keys(validationErrors).some((key) => validationErrors[key]) !Object.keys(validationErrors).some(key => validationErrors[key])
) { ) {
handleSave() handleSave()
} }
@@ -321,10 +321,10 @@ function CalibrationCondInfo({
key as keyof CalibrationConditions key as keyof CalibrationConditions
] as string ] as string
} }
onChange={(e) => onChange={e =>
handleFieldChange(key, e.target.value) handleFieldChange(key, e.target.value)
} }
onKeyDown={(e) => { onKeyDown={e => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault() e.preventDefault()
handleSave() handleSave()
@@ -334,8 +334,8 @@ function CalibrationCondInfo({
hasError hasError
? 'border-destructive focus-visible:ring-destructive' ? 'border-destructive focus-visible:ring-destructive'
: isValid : isValid
? 'border-green-500 focus-visible:ring-green-500' ? 'border-green-500 focus-visible:ring-green-500'
: '' : ''
}`} }`}
placeholder={placeholder} placeholder={placeholder}
type="number" type="number"
@@ -361,7 +361,7 @@ function CalibrationCondInfo({
</div> </div>
</div> </div>
) )
}, }
)} )}
</div> </div>
@@ -385,7 +385,7 @@ function CalibrationCondInfo({
disabled={ disabled={
isLoading || isLoading ||
Object.keys(validationErrors).some( Object.keys(validationErrors).some(
(key) => validationErrors[key], key => validationErrors[key]
) )
} }
> >
@@ -463,7 +463,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
<Input <Input
type="number" type="number"
value={config.defaultConditions?.temperature || '20'} value={config.defaultConditions?.temperature || '20'}
onChange={(e) => onChange={e =>
onChange({ onChange({
...config, ...config,
defaultConditions: { defaultConditions: {
@@ -489,7 +489,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
<Input <Input
type="number" type="number"
value={config.defaultConditions?.humidity || '65'} value={config.defaultConditions?.humidity || '65'}
onChange={(e) => onChange={e =>
onChange({ onChange({
...config, ...config,
defaultConditions: { defaultConditions: {
@@ -515,7 +515,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
<Input <Input
type="number" type="number"
value={config.defaultConditions?.pressure || '101.3'} value={config.defaultConditions?.pressure || '101.3'}
onChange={(e) => onChange={e =>
onChange({ onChange({
...config, ...config,
defaultConditions: { defaultConditions: {
@@ -541,7 +541,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
<Input <Input
type="number" type="number"
value={config.defaultConditions?.voltage || '220'} value={config.defaultConditions?.voltage || '220'}
onChange={(e) => onChange={e =>
onChange({ onChange({
...config, ...config,
defaultConditions: { defaultConditions: {
@@ -567,7 +567,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
<Input <Input
type="number" type="number"
value={config.defaultConditions?.frequency || '50'} value={config.defaultConditions?.frequency || '50'}
onChange={(e) => onChange={e =>
onChange({ onChange({
...config, ...config,
defaultConditions: { defaultConditions: {
@@ -632,7 +632,7 @@ export const calibrationConditionsDefinition: ElementDefinition<
<CalibrationCondInfo <CalibrationCondInfo
conditions={conditions} conditions={conditions}
isOutdated={isOutdated} isOutdated={isOutdated}
onUpdate={async (newConditions) => { onUpdate={async newConditions => {
onChange?.(newConditions) onChange?.(newConditions)
return true return true
}} }}

View File

@@ -1,4 +1,4 @@
import { Input } from '@/components/ui/input' import { Input } from '@/component/ui/input'
import { ElementDefinition } from '@/lib/element-registry' import { ElementDefinition } from '@/lib/element-registry'
import { Calendar } from 'lucide-react' import { Calendar } from 'lucide-react'
@@ -22,7 +22,7 @@ export const dateDefinition: ElementDefinition<DateConfig, string> = {
type="checkbox" type="checkbox"
id="required" id="required"
checked={config.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"> <label htmlFor="required" className="text-sm font-medium">
Обязательное поле Обязательное поле
@@ -42,7 +42,7 @@ export const dateDefinition: ElementDefinition<DateConfig, string> = {
<Input <Input
type="date" type="date"
value={value || ''} value={value || ''}
onChange={(e) => onChange?.(e.target.value)} onChange={e => onChange?.(e.target.value)}
required={config.required} required={config.required}
/> />
), ),

View File

@@ -1,4 +1,4 @@
import { Input } from '@/components/ui/input' import { Input } from '@/component/ui/input'
import { ElementDefinition } from '@/lib/element-registry' import { ElementDefinition } from '@/lib/element-registry'
import { Hash } from 'lucide-react' import { Hash } from 'lucide-react'
@@ -40,7 +40,7 @@ export const numberDefinition: ElementDefinition<NumberConfig, number> = {
type="number" type="number"
placeholder={config.placeholder || '0'} placeholder={config.placeholder || '0'}
value={value || ''} value={value || ''}
onChange={(e) => onChange?.(parseFloat(e.target.value) || 0)} onChange={e => onChange?.(parseFloat(e.target.value) || 0)}
required={config.required} required={config.required}
/> />
), ),

View File

@@ -1,5 +1,5 @@
import { Button } from '@/components/ui/button' import { Button } from '@/component/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/component/ui/input'
import { ElementDefinition } from '@/lib/element-registry' import { ElementDefinition } from '@/lib/element-registry'
import { ElementOption } from '@/type/template' import { ElementOption } from '@/type/template'
import { CheckSquare, Plus, Trash2 } from 'lucide-react' import { CheckSquare, Plus, Trash2 } from 'lucide-react'

View File

@@ -1,12 +1,12 @@
import { Button } from '@/components/ui/button' import { Button } from '@/component/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/component/ui/input'
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/component/ui/select'
import { ElementDefinition } from '@/lib/element-registry' import { ElementDefinition } from '@/lib/element-registry'
import { ElementOption } from '@/type/template' import { ElementOption } from '@/type/template'
import { ChevronDown, Plus, Trash2 } from 'lucide-react' import { ChevronDown, Plus, Trash2 } from 'lucide-react'

View File

@@ -1,4 +1,4 @@
import { Input } from '@/components/ui/input' import { Input } from '@/component/ui/input'
import { ElementDefinition } from '@/lib/element-registry' import { ElementDefinition } from '@/lib/element-registry'
import { Type } from 'lucide-react' import { Type } from 'lucide-react'
@@ -38,7 +38,7 @@ export const textDefinition: ElementDefinition<TextConfig, string> = {
<Input <Input
placeholder={config.placeholder || 'Введите текст'} placeholder={config.placeholder || 'Введите текст'}
value={value || ''} value={value || ''}
onChange={(e) => onChange?.(e.target.value)} onChange={e => onChange?.(e.target.value)}
required={config.required} required={config.required}
/> />
), ),

View File

@@ -1,4 +1,4 @@
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/component/ui/textarea'
import { ElementDefinition } from '@/lib/element-registry' import { ElementDefinition } from '@/lib/element-registry'
import { FileText } from 'lucide-react' import { FileText } from 'lucide-react'
@@ -39,7 +39,7 @@ export const textareaDefinition: ElementDefinition<TextareaConfig, string> = {
<Textarea <Textarea
placeholder={config.placeholder || 'Введите текст'} placeholder={config.placeholder || 'Введите текст'}
value={value || ''} value={value || ''}
onChange={(e) => onChange?.(e.target.value)} onChange={e => onChange?.(e.target.value)}
required={config.required} required={config.required}
rows={3} rows={3}
className="w-full resize-none" className="w-full resize-none"

View File

@@ -1,7 +1,7 @@
import { Badge } from '@/components/ui/badge' import { Badge } from '@/component/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/component/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/component/ui/separator'
import { Calendar, Check, FileText, Settings } from 'lucide-react' import { Calendar, Check, FileText, Settings } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
@@ -41,15 +41,15 @@ export function StandardsDialog({
registryNumber, registryNumber,
}: StandardsDialogProps) { }: StandardsDialogProps) {
const [selectedStandards, setSelectedStandards] = useState<string[]>( const [selectedStandards, setSelectedStandards] = useState<string[]>(
config.selectedStandards, config.selectedStandards
) )
if (!isOpen) return null if (!isOpen) return null
const toggleStandard = (standardId: string) => { const toggleStandard = (standardId: string) => {
setSelectedStandards((prev) => { setSelectedStandards(prev => {
if (prev.includes(standardId)) { if (prev.includes(standardId)) {
return prev.filter((id) => id !== standardId) return prev.filter(id => id !== standardId)
} else if (prev.length < 7) { } else if (prev.length < 7) {
return [...prev, standardId] return [...prev, standardId]
} }
@@ -93,7 +93,7 @@ export function StandardsDialog({
<CardContent className="space-y-4"> <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"> <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 isSelected = selectedStandards.includes(standard.id)
const isExpiring = isExpiringSoon(standard.validUntil) const isExpiring = isExpiringSoon(standard.validUntil)
@@ -155,7 +155,7 @@ export function StandardsDialog({
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
Действует до:{' '} Действует до:{' '}
{new Date(standard.validUntil).toLocaleDateString( {new Date(standard.validUntil).toLocaleDateString(
'ru-RU', 'ru-RU'
)} )}
</div> </div>
</div> </div>

View File

@@ -1,12 +1,12 @@
import { Button } from '@/components/ui/button' import { Button } from '@/component/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/component/ui/input'
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/component/ui/select'
import { CellTarget } from '@/type/template' import { CellTarget } from '@/type/template'
import { Calendar, Settings, Trash2 } from 'lucide-react' import { Calendar, Settings, Trash2 } from 'lucide-react'

View File

@@ -1,24 +1,26 @@
import { Badge } from "@/components/ui/badge"; import { Badge } from '@/component/ui/badge'
import { Button } from "@/components/ui/button"; import { Button } from '@/component/ui/button'
import { Calendar, Settings } from "lucide-react"; import { Calendar, Settings } from 'lucide-react'
interface StandardsConfig { interface StandardsConfig {
registryNumber: string; registryNumber: string
sheet: string; sheet: string
startCell: string; startCell: string
maxItems: number; maxItems: number
targetCells: any[]; targetCells: any[]
} }
interface StandardsPreviewProps { interface StandardsPreviewProps {
config: StandardsConfig; config: StandardsConfig
} }
export const StandardsPreview: React.FC<StandardsPreviewProps> = ({ config }) => { export const StandardsPreview: React.FC<StandardsPreviewProps> = ({
config,
}) => {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<div className="text-sm font-medium text-gray-700">Предпросмотр</div> <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="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings className="h-4 w-4 text-gray-500" /> <Settings className="h-4 w-4 text-gray-500" />
@@ -27,7 +29,7 @@ export const StandardsPreview: React.FC<StandardsPreviewProps> = ({ config }) =>
<div className="space-y-2"> <div className="space-y-2">
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
ГРСИ: {config.registryNumber || "Не указан"} ГРСИ: {config.registryNumber || 'Не указан'}
</div> </div>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
Максимум: {config.maxItems} эталонов Максимум: {config.maxItems} эталонов
@@ -36,7 +38,7 @@ export const StandardsPreview: React.FC<StandardsPreviewProps> = ({ config }) =>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="outline" size="sm" disabled className="text-xs"> <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> </Button>
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
@@ -45,11 +47,11 @@ export const StandardsPreview: React.FC<StandardsPreviewProps> = ({ config }) =>
</div> </div>
{config.targetCells && config.targetCells.length > 0 && ( {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) => ( {config.targetCells.map((target, index) => (
<span <span
key={index} 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} title={target.displayName}
> >
{target.sheet}!{target.cell} {target.sheet}!{target.cell}
@@ -60,5 +62,5 @@ export const StandardsPreview: React.FC<StandardsPreviewProps> = ({ config }) =>
</div> </div>
</div> </div>
</div> </div>
); )
}; }

View File

@@ -1,188 +1,198 @@
import { Badge } from "@/components/ui/badge"; import { Badge } from '@/component/ui/badge'
import { Button } from "@/components/ui/button"; import { Button } from '@/component/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import {
import { Calendar, Check, FileText, Settings, X } from "lucide-react"; Dialog,
import { useState } from "react"; DialogContent,
DialogHeader,
DialogTitle,
} from '@/component/ui/dialog'
import { Calendar, Check, FileText, Settings, X } from 'lucide-react'
import { useState } from 'react'
interface MeasurementStandard { interface MeasurementStandard {
id: string; id: string
name: string; name: string
shortName: string; shortName: string
type: string; type: string
registryNumber: string; registryNumber: string
range: string; range: string
accuracy: string; accuracy: string
certificateNumber: string; certificateNumber: string
validUntil: string; validUntil: string
} }
interface StandardsSelectorProps { interface StandardsSelectorProps {
isOpen: boolean; isOpen: boolean
onClose: () => void; onClose: () => void
value: string[]; value: string[]
onChange: (value: string[]) => void; onChange: (value: string[]) => void
registryNumber?: string; registryNumber?: string
} }
// Моковые данные для эталонов // Моковые данные для эталонов
const mockStandards: MeasurementStandard[] = [ const mockStandards: MeasurementStandard[] = [
{ {
id: "1", id: '1',
name: "Эталон массы 1 кг", name: 'Эталон массы 1 кг',
shortName: "ЭМ-1кг", shortName: 'ЭМ-1кг',
type: "Масса", type: 'Масса',
registryNumber: "ГРСИ 12345-01", registryNumber: 'ГРСИ 12345-01',
range: "0.5-2 кг", range: '0.5-2 кг',
accuracy: "±0.001 г", accuracy: '±0.001 г',
certificateNumber: "СИ-2024-001", certificateNumber: 'СИ-2024-001',
validUntil: "2025-12-31", validUntil: '2025-12-31',
}, },
{ {
id: "2", id: '2',
name: "Эталон длины 1 м", name: 'Эталон длины 1 м',
shortName: "ЭД-1м", shortName: 'ЭД-1м',
type: "Длина", type: 'Длина',
registryNumber: "ГРСИ 12345-02", registryNumber: 'ГРСИ 12345-02',
range: "0.5-2 м", range: '0.5-2 м',
accuracy: "±0.001 мм", accuracy: '±0.001 мм',
certificateNumber: "СИ-2024-002", certificateNumber: 'СИ-2024-002',
validUntil: "2025-06-30", validUntil: '2025-06-30',
}, },
{ {
id: "3", id: '3',
name: "Эталон температуры 20°C", name: 'Эталон температуры 20°C',
shortName: "ЭТ-20°C", shortName: 'ЭТ-20°C',
type: "Температура", type: 'Температура',
registryNumber: "ГРСИ 12345-03", registryNumber: 'ГРСИ 12345-03',
range: "15-25°C", range: '15-25°C',
accuracy: "±0.01°C", accuracy: '±0.01°C',
certificateNumber: "СИ-2024-003", certificateNumber: 'СИ-2024-003',
validUntil: "2025-03-15", validUntil: '2025-03-15',
}, },
{ {
id: "4", id: '4',
name: "Эталон давления 1 Па", name: 'Эталон давления 1 Па',
shortName: "ЭД-1Па", shortName: 'ЭД-1Па',
type: "Давление", type: 'Давление',
registryNumber: "ГРСИ 12345-04", registryNumber: 'ГРСИ 12345-04',
range: "0.5-2 Па", range: '0.5-2 Па',
accuracy: "±0.001 Па", accuracy: '±0.001 Па',
certificateNumber: "СИ-2024-004", certificateNumber: 'СИ-2024-004',
validUntil: "2025-09-20", validUntil: '2025-09-20',
}, },
{ {
id: "5", id: '5',
name: "Эталон времени 1 с", name: 'Эталон времени 1 с',
shortName: "ЭВ-1с", shortName: 'ЭВ-1с',
type: "Время", type: 'Время',
registryNumber: "ГРСИ 12345-05", registryNumber: 'ГРСИ 12345-05',
range: "0.5-2 с", range: '0.5-2 с',
accuracy: "±0.000001 с", accuracy: '±0.000001 с',
certificateNumber: "СИ-2024-005", certificateNumber: 'СИ-2024-005',
validUntil: "2025-12-01", validUntil: '2025-12-01',
}, },
]; ]
export function StandardsSelector({ export function StandardsSelector({
isOpen, isOpen,
onClose, onClose,
value = [], value = [],
onChange, onChange,
registryNumber = "ГРСИ 12345" registryNumber = 'ГРСИ 12345',
}: StandardsSelectorProps) { }: StandardsSelectorProps) {
const [selectedStandards, setSelectedStandards] = useState<string[]>(value); const [selectedStandards, setSelectedStandards] = useState<string[]>(value)
const toggleStandard = (standardId: string) => { const toggleStandard = (standardId: string) => {
setSelectedStandards(prev => { setSelectedStandards(prev => {
if (prev.includes(standardId)) { if (prev.includes(standardId)) {
return prev.filter(id => id !== standardId); return prev.filter(id => id !== standardId)
} else if (prev.length < 7) { } else if (prev.length < 7) {
return [...prev, standardId]; return [...prev, standardId]
} }
return prev; return prev
}); })
}; }
const handleSave = () => { const handleSave = () => {
onChange(selectedStandards); onChange(selectedStandards)
onClose(); onClose()
}; }
const handleClose = () => { const handleClose = () => {
setSelectedStandards(value); // Сбрасываем к исходному значению setSelectedStandards(value) // Сбрасываем к исходному значению
onClose(); onClose()
}; }
const isExpiringSoon = (validUntil: string) => { const isExpiringSoon = (validUntil: string) => {
const expiryDate = new Date(validUntil); const expiryDate = new Date(validUntil)
const now = new Date(); const now = new Date()
const monthsUntilExpiry = (expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24 * 30); const monthsUntilExpiry =
return monthsUntilExpiry <= 3; (expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24 * 30)
}; return monthsUntilExpiry <= 3
}
return ( return (
<Dialog open={isOpen} onOpenChange={handleClose}> <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> <DialogHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<DialogTitle className="text-lg flex items-center gap-2"> <DialogTitle className="flex items-center gap-2 text-lg">
<Settings className="w-5 h-5" /> <Settings className="h-5 w-5" />
Измерительные эталоны Измерительные эталоны
</DialogTitle> </DialogTitle>
<p className="text-sm text-muted-foreground mt-1"> <p className="mt-1 text-sm text-muted-foreground">
ГРСИ: {registryNumber} Выбрано: {selectedStandards.length}/7 ГРСИ: {registryNumber} Выбрано: {selectedStandards.length}/7
</p> </p>
</div> </div>
<Button variant="ghost" size="sm" onClick={handleClose}> <Button variant="ghost" size="sm" onClick={handleClose}>
<X className="w-4 h-4" /> <X className="h-4 w-4" />
</Button> </Button>
</div> </div>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-96 overflow-y-auto"> <div className="grid max-h-96 grid-cols-1 gap-3 overflow-y-auto md:grid-cols-2">
{mockStandards.map((standard) => { {mockStandards.map(standard => {
const isSelected = selectedStandards.includes(standard.id); const isSelected = selectedStandards.includes(standard.id)
const isExpiring = isExpiringSoon(standard.validUntil); const isExpiring = isExpiringSoon(standard.validUntil)
return ( return (
<div <div
key={standard.id} key={standard.id}
onClick={() => toggleStandard(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 isSelected
? "border-primary bg-primary/10 shadow-sm" ? 'border-primary bg-primary/10 shadow-sm'
: "border-border hover:border-primary/50 hover:bg-primary/5" : 'border-border hover:border-primary/50 hover:bg-primary/5'
}`} }`}
> >
<div className="flex items-start gap-3"> <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 ${ <div
isSelected className={`mt-1 flex h-5 w-5 items-center justify-center rounded-full border-2 transition-colors ${
? "border-primary bg-primary" isSelected
: "border-border" ? 'border-primary bg-primary'
}`}> : 'border-border'
{isSelected && <Check className="w-3 h-3 text-primary-foreground" />} }`}
>
{isSelected && (
<Check className="h-3 w-3 text-primary-foreground" />
)}
</div> </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 items-start justify-between gap-2">
<div className="flex-1"> <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} {standard.shortName}
</div> </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} {standard.name}
</div> </div>
</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"> <Badge variant="secondary" className="text-xs">
{standard.accuracy} {standard.accuracy}
</Badge> </Badge>
{isExpiring && ( {isExpiring && (
<Badge variant="destructive" className="text-xs"> <Badge variant="destructive" className="text-xs">
<Calendar className="w-3 h-3 mr-1" /> <Calendar className="mr-1 h-3 w-3" />
Истекает Истекает
</Badge> </Badge>
)} )}
@@ -191,24 +201,27 @@ export function StandardsSelector({
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1">
<div className="flex items-center gap-1 text-xs text-muted-foreground"> <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} {standard.registryNumber}
</div> </div>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
Диапазон: {standard.range} Диапазон: {standard.range}
</div> </div>
<div className="text-xs text-muted-foreground"> <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> </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"> <div className="text-sm text-muted-foreground">
Максимум 7 эталонов. Выбрано: {selectedStandards.length} Максимум 7 эталонов. Выбрано: {selectedStandards.length}
</div> </div>
@@ -216,13 +229,11 @@ export function StandardsSelector({
<Button variant="outline" onClick={handleClose}> <Button variant="outline" onClick={handleClose}>
Отмена Отмена
</Button> </Button>
<Button onClick={handleSave}> <Button onClick={handleSave}>Сохранить</Button>
Сохранить
</Button>
</div> </div>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); )
} }

View File

@@ -1,19 +1,17 @@
import { FileText, Settings, Wrench } from 'lucide-react' import { FileText, Settings, Wrench } from 'lucide-react'
import React from 'react' import React from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Template } from '../../type/template' import { Template } from '../type/template'
import { Button } from '../ui/button' import { Button } from './ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card' import { Card, CardContent, CardHeader, CardTitle } from './ui/card'
interface TemplateCardProps { interface TemplateCardProps {
template: Template template: Template
onDelete: (template: Template) => void
isSelected?: boolean isSelected?: boolean
} }
const TemplateCard: React.FC<TemplateCardProps> = ({ const TemplateCard: React.FC<TemplateCardProps> = ({
template, template,
onDelete,
isSelected = false, isSelected = false,
}) => { }) => {
const navigate = useNavigate() const navigate = useNavigate()

View File

@@ -1,4 +1,4 @@
import { Button } from '@/components/ui/button' import { Button } from '@/component/ui/button'
import { FileText, Upload } from 'lucide-react' import { FileText, Upload } from 'lucide-react'
import React, { useRef } from 'react' import React, { useRef } from 'react'

View File

@@ -1,5 +1,5 @@
import { Button } from '@/components/ui/button' import { Button } from '@/component/ui/button'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/component/ui/separator'
import { import {
AlignCenter, AlignCenter,
AlignLeft, AlignLeft,

View File

@@ -1,4 +1,4 @@
import { Button } from '@/components/ui/button' import { Button } from '@/component/ui/button'
import { Template } from '@/type/template' import { Template } from '@/type/template'
import { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react' import { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react'
import React from 'react' import React from 'react'

View File

@@ -1,4 +1,4 @@
import DualSpreadsheet from '@/components/DualSpreadsheet/DualSpreadsheet' import DualSpreadsheet from '@/component/DualSpreadsheet/DualSpreadsheet'
import { ExcelParseResult } from '@/service/fileApiSevice' import { ExcelParseResult } from '@/service/fileApiSevice'
import React from 'react' import React from 'react'

View File

@@ -1,4 +1,4 @@
import { useTemplateContext } from '@/context/TemplateContext' import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
import { import {
getFileData, getFileData,
getLatestFileForTemplate, getLatestFileForTemplate,

View File

@@ -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 { Circle, CircleCheck, FileText, Plus, Trash2 } from 'lucide-react'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useTemplateContext } from '../../context/TemplateContext' import TemplateCard from '../TemplateCard'
import { Template } from '../../type/template'
import { Button } from '../ui/button' import { Button } from '../ui/button'
import { import {
Dialog, Dialog,
@@ -13,16 +14,14 @@ import {
DialogTrigger, DialogTrigger,
} from '../ui/dialog' } from '../ui/dialog'
import { Input } from '../ui/input' import { Input } from '../ui/input'
import TemplateCard from './TemplateCard'
import { TemplateEditor } from './TemplateEditor' import { TemplateEditor } from './TemplateEditor'
interface TemplateManagerProps { interface TemplateManagerProps {
onTemplateSelect?: (template: Template) => void template?: Template
templateId?: string
} }
export const TemplateManager: React.FC<TemplateManagerProps> = ({ export const TemplateManager: React.FC<TemplateManagerProps> = ({
templateId, template: initialTemplate,
}) => { }) => {
const navigate = useNavigate() const navigate = useNavigate()
const { const {
@@ -32,7 +31,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
addTemplate, addTemplate,
updateTemplate, updateTemplate,
deleteTemplate, deleteTemplate,
loadTemplates, refetchTemplates,
} = useTemplateContext() } = useTemplateContext()
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>( const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
null null
@@ -50,14 +49,11 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
// Автоматически открываем редактирование шаблона, если передан templateId // Автоматически открываем редактирование шаблона, если передан templateId
useEffect(() => { useEffect(() => {
if (templateId) { if (initialTemplate) {
const template = templates.find(t => t.id === templateId) setSelectedTemplate(initialTemplate)
if (template) { setIsEditMode(true)
setSelectedTemplate(template)
setIsEditMode(true)
}
} }
}, [templateId, templates]) }, [initialTemplate])
const handleCreateTemplate = async () => { const handleCreateTemplate = async () => {
if (!newTemplateName.trim()) return if (!newTemplateName.trim()) return
@@ -70,58 +66,44 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
elements: [], elements: [],
layoutSettings: undefined, layoutSettings: undefined,
}) })
// Находим созданный шаблон и открываем его для редактирования
// Поскольку addTemplate может занять время, используем небольшую задержку
setTimeout(() => {
const newTemplate = templates.find(
t => t.name === newTemplateName.trim()
)
if (newTemplate) {
setSelectedTemplate(newTemplate)
setIsEditMode(true)
}
}, 100)
setIsCreateDialogOpen(false) setIsCreateDialogOpen(false)
setNewTemplateName('') setNewTemplateName('')
setNewTemplateDescription('') setNewTemplateDescription('')
} catch (error) { await refetchTemplates()
console.error('Ошибка при создании шаблона:', error) const created = templates.find(t => t.name === newTemplateName.trim())
// Здесь можно добавить уведомление пользователю об ошибке if (created) {
setSelectedTemplate(created)
setIsEditMode(true)
}
} catch (err) {
console.error('Error creating template:', err)
alert('Ошибка при создании шаблона. Попробуйте еще раз.') alert('Ошибка при создании шаблона. Попробуйте еще раз.')
} finally { } finally {
setIsCreating(false) setIsCreating(false)
} }
} }
const handleTemplateUpdate = async (updatedTemplate: Template) => { // const handleTemplateUpdate = async (updatedTemplate: Template) => {
try { // try {
await updateTemplate(updatedTemplate) // await updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate) // setSelectedTemplate(updatedTemplate)
} catch (error) { // } catch (err) {
console.error('Ошибка при обновлении шаблона:', error) // console.error('Error updating template:', err)
alert('Ошибка при обновлении шаблона. Попробуйте еще раз.') // alert('Ошибка при обновлении шаблона. Попробуйте еще раз.')
} // }
} // }
const handleDeleteTemplates = () => { const handleDeleteTemplates = () => {
templatesToDelete.forEach(template => { templatesToDelete.forEach(t => deleteTemplate(t.id))
deleteTemplate(template.id)
})
setTemplatesToDelete([]) setTemplatesToDelete([])
setIsSelectionMode(false) setIsSelectionMode(false)
setSelectedTemplates(new Set()) setSelectedTemplates(new Set())
} }
const toggleTemplateSelection = (templateId: string) => { const toggleTemplateSelection = (id: UUID) => {
const newSelected = new Set(selectedTemplates) const next = new Set(selectedTemplates)
if (newSelected.has(templateId)) { next.has(id) ? next.delete(id) : next.add(id)
newSelected.delete(templateId) setSelectedTemplates(next)
} else {
newSelected.add(templateId)
}
setSelectedTemplates(newSelected)
} }
const handleSelectionModeToggle = () => { const handleSelectionModeToggle = () => {
@@ -135,14 +117,12 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
} }
const handleDeleteSelected = () => { const handleDeleteSelected = () => {
const selectedTemplatesList = templates.filter(t => const toDelete = templates.filter(t => selectedTemplates.has(t.id))
selectedTemplates.has(t.id) setTemplatesToDelete(toDelete)
)
setTemplatesToDelete(selectedTemplatesList)
} }
const handleRetry = () => { const handleRetry = () => {
loadTemplates() refetchTemplates()
} }
if (isEditMode && selectedTemplate) { if (isEditMode && selectedTemplate) {
@@ -152,9 +132,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
onBack={() => { onBack={() => {
setIsEditMode(false) setIsEditMode(false)
setSelectedTemplate(null) 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 className="mb-2 text-lg font-medium text-gray-900">
Ошибка загрузки шаблонов Ошибка загрузки шаблонов
</h3> </h3>
<p className="mb-4 text-gray-600">{error}</p> <p className="mb-4 text-gray-600">{error.message}</p>
<Button onClick={handleRetry}>Попробовать снова</Button> <Button onClick={handleRetry}>Попробовать снова</Button>
</div> </div>
</div> </div>

View File

@@ -1,235 +0,0 @@
import React, {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import { getDefaultLayoutSettings, migrateTemplateElement } from '../lib/utils'
import {
apiTemplateToTemplate,
createTemplate as createTemplateApi,
getTemplate as getTemplateApi,
getTemplates,
patchTemplate,
templateToCreateInput,
templateToPatchInput,
} from '../service/templateApiService'
import { Template, TemplateElement } from '../type/template'
interface TemplateContextType {
templates: Template[]
isLoading: boolean
error: string | null
addTemplate: (
template: Omit<Template, 'id' | 'createdAt' | 'updatedAt'>
) => Promise<void>
updateTemplate: (template: Template) => Promise<void>
deleteTemplate: (templateId: string) => void
getTemplate: (templateId: string) => Template | undefined
loadTemplates: () => Promise<void>
refreshTemplate: (templateId: string) => Promise<void>
}
const TemplateContext = createContext<TemplateContextType | undefined>(
undefined
)
export const useTemplateContext = () => {
const context = useContext(TemplateContext)
if (!context) {
throw new Error('useTemplateContext must be used within a TemplateProvider')
}
return context
}
interface TemplateProviderProps {
children: ReactNode
}
// Функция создания элемента названия протокола
function createProtocolNameElement(): TemplateElement {
return {
id: 'protocol-name-' + Date.now(),
type: 'text',
name: 'protocolName',
label: 'Название протокола',
placeholder: 'Введите название протокола',
required: true,
targetCells: [
{ sheet: 'L', cell: 'A1', displayName: 'Название протокола' },
],
options: [],
order: 0,
layout: {
x: 50,
y: 50,
width: 400,
height: 80,
zIndex: 1,
},
}
}
// Функция миграции шаблона
function migrateTemplate(template: any): Template {
return {
...template,
elements: template.elements?.map(migrateTemplateElement) || [],
layoutSettings: template.layoutSettings || getDefaultLayoutSettings(),
createdAt: template.createdAt ? new Date(template.createdAt) : new Date(),
updatedAt: template.updatedAt ? new Date(template.updatedAt) : new Date(),
}
}
export const TemplateProvider: React.FC<TemplateProviderProps> = ({
children,
}) => {
const [templates, setTemplates] = useState<Template[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
// Загрузка всех шаблонов с API
const loadTemplates = useCallback(async () => {
setIsLoading(true)
setError(null)
try {
const response = await getTemplates()
const convertedTemplates = response.templates.map(apiTemplateToTemplate)
setTemplates(convertedTemplates)
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Неизвестная ошибка'
setError(errorMessage)
console.error('Ошибка при загрузке шаблонов:', err)
} finally {
setIsLoading(false)
}
}, [])
// Загрузка шаблонов при монтировании компонента
useEffect(() => {
loadTemplates()
}, [loadTemplates])
// Обновление конкретного шаблона с API
const refreshTemplate = useCallback(async (templateId: string) => {
try {
const response = await getTemplateApi(templateId)
if (response.template) {
const convertedTemplate = apiTemplateToTemplate(response.template)
setTemplates(prev =>
prev.map(t => (t.id === templateId ? convertedTemplate : t))
)
}
} catch (err) {
console.error('Ошибка при обновлении шаблона:', err)
}
}, [])
const addTemplate = async (
templateData: Omit<Template, 'id' | 'createdAt' | 'updatedAt'>
) => {
// Проверяем есть ли уже элемент названия протокола
const hasProtocolName = templateData.elements.some(
el => el.name === 'protocolName' || el.label === 'Название протокола'
)
let templateWithProtocolName = templateData
if (!hasProtocolName) {
// Добавляем элемент названия протокола как первый элемент
const protocolNameElement = createProtocolNameElement()
templateWithProtocolName = {
...templateData,
elements: [
protocolNameElement,
...templateData.elements.map(el => ({
...el,
order: (el.order || 0) + 1,
})),
],
}
}
setIsLoading(true)
setError(null)
try {
const createInput = templateToCreateInput(templateWithProtocolName)
const response = await createTemplateApi(createInput)
// Получаем созданный шаблон с сервера
const templateResponse = await getTemplateApi(response.id)
if (templateResponse.template) {
const newTemplate = apiTemplateToTemplate(templateResponse.template)
setTemplates(prev => [...prev, newTemplate])
}
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Неизвестная ошибка'
setError(errorMessage)
console.error('Ошибка при создании шаблона:', err)
throw err
} finally {
setIsLoading(false)
}
}
const updateTemplate = async (updatedTemplate: Template) => {
console.log('updateTemplate called with:', updatedTemplate)
console.log('Elements count:', updatedTemplate.elements.length)
setIsLoading(true)
setError(null)
try {
const patchInput = templateToPatchInput(updatedTemplate)
await patchTemplate(updatedTemplate.id, patchInput)
// Обновляем локальное состояние
const migratedTemplate = migrateTemplate(updatedTemplate)
console.log('Migrated template:', migratedTemplate)
console.log('Migrated elements count:', migratedTemplate.elements.length)
setTemplates(prev =>
prev.map(t => (t.id === migratedTemplate.id ? migratedTemplate : t))
)
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Неизвестная ошибка'
setError(errorMessage)
console.error('Ошибка при обновлении шаблона:', err)
throw err
} finally {
setIsLoading(false)
}
}
const deleteTemplate = (templateId: string) => {
// Пока удаление только локально, так как в API нет endpoint для удаления
// В будущем здесь можно добавить вызов API для удаления
setTemplates(prev => prev.filter(t => t.id !== templateId))
}
const getTemplate = (templateId: string) => {
const template = templates.find(t => t.id === templateId)
return template ? migrateTemplate(template) : undefined
}
return (
<TemplateContext.Provider
value={{
templates: templates.map(migrateTemplate), // Применяем миграцию при получении
isLoading,
error,
addTemplate,
updateTemplate,
deleteTemplate,
getTemplate,
loadTemplates,
refreshTemplate,
}}
>
{children}
</TemplateContext.Provider>
)
}

View File

@@ -5,7 +5,7 @@ export interface CreateTemplateInput {
} }
export interface CreateTemplateOutput { export interface CreateTemplateOutput {
id: string id: UUID
} }
export interface GetTemplatesOutput { export interface GetTemplatesOutput {
@@ -37,8 +37,8 @@ export interface ApiTemplate {
} }
// Импортируем типы для преобразования // Импортируем типы для преобразования
import { getDefaultLayoutSettings, migrateTemplateElement } from '../lib/utils' import { getDefaultLayoutSettings, migrateTemplateElement } from '@/lib/utils'
import { Template, TemplateElement } from '../type/template' import { Template, TemplateElement } from '@/type/template'
// Утилитарные функции для преобразования данных // Утилитарные функции для преобразования данных
@@ -93,7 +93,7 @@ export function templateToPatchInput(template: Template): PatchTemplateInput {
} }
// API создание шаблона // API создание шаблона
export async function createTemplate( export async function createTemplateApi(
template: CreateTemplateInput template: CreateTemplateInput
): Promise<CreateTemplateOutput> { ): Promise<CreateTemplateOutput> {
try { try {
@@ -118,7 +118,7 @@ export async function createTemplate(
} }
// API получение всех шаблонов // API получение всех шаблонов
export async function getTemplates(): Promise<GetTemplatesOutput> { export async function getTemplatesApi(): Promise<GetTemplatesOutput> {
try { try {
const response = await fetch('/api/templates/', { const response = await fetch('/api/templates/', {
method: 'GET', method: 'GET',
@@ -140,8 +140,8 @@ export async function getTemplates(): Promise<GetTemplatesOutput> {
} }
// API получение конкретного шаблона // API получение конкретного шаблона
export async function getTemplate( export async function getTemplateApi(
templateId: string templateId: UUID
): Promise<GetTemplateOutput> { ): Promise<GetTemplateOutput> {
try { try {
const response = await fetch(`/api/templates/${templateId}`, { const response = await fetch(`/api/templates/${templateId}`, {
@@ -164,7 +164,7 @@ export async function getTemplate(
} }
// API обновление шаблона // API обновление шаблона
export async function patchTemplate( export async function patchTemplateApi(
templateId: string, templateId: string,
template: PatchTemplateInput template: PatchTemplateInput
): Promise<PatchTemplateOutput> { ): Promise<PatchTemplateOutput> {
@@ -194,5 +194,5 @@ export async function updateTemplate(
template: Template template: Template
): Promise<PatchTemplateOutput> { ): Promise<PatchTemplateOutput> {
const patchInput = templateToPatchInput(template) const patchInput = templateToPatchInput(template)
return patchTemplate(template.id, patchInput) return patchTemplateApi(template.id, patchInput)
} }

View File

@@ -0,0 +1,143 @@
import {
apiTemplateToTemplate,
createTemplateApi,
getTemplateApi,
getTemplatesApi,
patchTemplateApi,
templateToCreateInput,
templateToPatchInput,
} from '@/entitiy/template/api/templateApiService'
import { Template } from '@/type/template'
import {
QueryClient,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import React, { createContext, ReactNode, useContext, useMemo } from 'react'
export const queryClient = new QueryClient()
interface TemplateContextType {
templates: Template[]
isLoading: boolean
error: Error | null
refetchTemplates: () => void
addTemplate: (
data: Omit<Template, 'id' | 'createdAt' | 'updatedAt'>
) => Promise<Template>
updateTemplate: (template: Template) => Promise<Template>
deleteTemplate: (id: UUID) => Promise<string>
}
const TemplateContext = createContext<TemplateContextType | undefined>(
undefined
)
export const TemplateProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const client = useQueryClient()
const {
data: templates = [],
status: listStatus,
error: errorList,
refetch,
} = useQuery<Template[], Error>({
queryKey: ['templates'],
queryFn: async () => {
const { templates: apiList } = await getTemplatesApi()
return apiList.map(apiTemplateToTemplate)
},
})
const createMutation = useMutation({
mutationFn: async (
newTemplate: Omit<Template, 'id' | 'createdAt' | 'updatedAt'>
) => {
const input = templateToCreateInput(newTemplate)
const { id } = await createTemplateApi(input)
const { template: apiTpl } = await getTemplateApi(id)
if (!apiTpl) throw new Error('Template not found')
return apiTemplateToTemplate(apiTpl)
},
onSuccess: () => {
client.invalidateQueries({ queryKey: ['templates'] })
},
})
const updateMutation = useMutation({
mutationFn: async (template: Template) => {
const input = templateToPatchInput(template)
await patchTemplateApi(template.id, input)
return template
},
onSuccess: updated => {
client.setQueryData<Template[]>(
['templates'],
old =>
old?.map((t: Template) => (t.id === updated.id ? updated : t)) ?? []
)
},
})
const deleteMutation = useMutation({
mutationFn: async (id: string) => {
// TODO: deleteTemplateApi
return id
},
onSuccess: id => {
client.setQueryData<Template[]>(
['templates'],
old => old?.filter((t: Template) => t.id !== id) ?? []
)
},
})
const value = useMemo(
() => ({
templates,
isLoading:
listStatus === 'pending' ||
createMutation.status === 'pending' ||
updateMutation.status === 'pending' ||
deleteMutation.status === 'pending',
error:
(errorList as Error) ||
(createMutation.error as Error) ||
(updateMutation.error as Error) ||
(deleteMutation.error as Error) ||
null,
refetchTemplates: refetch,
addTemplate: createMutation.mutateAsync,
updateTemplate: updateMutation.mutateAsync,
deleteTemplate: deleteMutation.mutateAsync,
}),
[
templates,
listStatus,
errorList,
createMutation.status,
createMutation.error,
updateMutation.status,
updateMutation.error,
deleteMutation.status,
deleteMutation.error,
refetch,
]
)
return (
<TemplateContext.Provider value={value}>
{children}
</TemplateContext.Provider>
)
}
export const useTemplateContext = (): TemplateContextType => {
const ctx = useContext(TemplateContext)
if (!ctx)
throw new Error('useTemplateContext must be used within TemplateProvider')
return ctx
}

View File

@@ -8,8 +8,8 @@ import {
selectDefinition, selectDefinition,
textareaDefinition, textareaDefinition,
textDefinition, textDefinition,
} from '@/components/BasicElements' } from '@/component/BasicElements'
import { standardsDefinition } from '@/components/StandardsElement/definition' import { standardsDefinition } from '@/component/StandardsElement/definition'
import { registerElement } from './element-registry' import { registerElement } from './element-registry'
// Регистрация всех элементов // Регистрация всех элементов

View File

@@ -1,6 +1,6 @@
import { ElementConstructor } from '@/components/TemplateManager/ElementConstructor' import { ElementConstructor } from '@/component/TemplateManager/ElementConstructor'
import { Button } from '@/components/ui/button' import { Button } from '@/component/ui/button'
import { useTemplateContext } from '@/context/TemplateContext' import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
import { Template, TemplateElement } from '@/type/template' import { Template, TemplateElement } from '@/type/template'
import { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react' import { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'

View File

@@ -1,20 +1,20 @@
import DualSpreadsheet from '@/components/DualSpreadsheet/DualSpreadsheet' import DualSpreadsheet from '@/component/DualSpreadsheet/DualSpreadsheet'
import { StandardsSelector } from '@/components/StandardsElement/StandardsSelector' import { StandardsSelector } from '@/component/StandardsElement/StandardsSelector'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/component/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/component/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/component/ui/checkbox'
import { Input } from '@/components/ui/input' import { Input } from '@/component/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/component/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { RadioGroup, RadioGroupItem } from '@/component/ui/radio-group'
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/component/ui/select'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/component/ui/textarea'
import { useTemplateContext } from '@/context/TemplateContext' import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
import { getElementDefinition } from '@/lib/element-registry' import { getElementDefinition } from '@/lib/element-registry'
import { getLatestFileForTemplate } from '@/service/fileApiSevice' import { getLatestFileForTemplate } from '@/service/fileApiSevice'
import { Template, TemplateElement } from '@/type/template' import { Template, TemplateElement } from '@/type/template'

View File

@@ -0,0 +1,17 @@
import { TemplateEditor } from '@/component/TemplateManager/TemplateEditor'
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
import { useNavigate, useParams } from 'react-router-dom'
export const TemplateEditPage = () => {
const { templateId } = useParams<{ templateId: string }>()
const { templates } = useTemplateContext()
const navigate = useNavigate()
const template = templates.find(t => t.id === templateId)
if (!template) {
navigate('/templates', { replace: true })
return null
}
return <TemplateEditor template={template} onBack={() => navigate(-1)} />
}

View File

@@ -1,20 +0,0 @@
import { TemplateManager } from '@/components/TemplateManager/TemplateManager'
import { useTemplateContext } from '@/context/TemplateContext'
import { FC, useEffect } from 'react'
import { useParams } from 'react-router-dom'
export const TemplateSetup: FC = () => {
const { templateId } = useParams<{ templateId: string }>()
const { templates } = useTemplateContext()
useEffect(() => {
if (templateId) {
const template = templates.find(t => t.id === templateId)
if (template) {
console.log('Opening template for editing:', template)
}
}
}, [templateId, templates])
return <TemplateManager templateId={templateId} />
}

View File

@@ -1 +0,0 @@
export { TemplateSetup } from './Page'

View File

@@ -0,0 +1,46 @@
import TemplateCard from '@/component/TemplateCard'
import { Button } from '@/component/ui/button'
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
import { Plus, Trash2 } from 'lucide-react'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
export const TemplatesOverviewPage = () => {
const navigate = useNavigate()
const { templates, deleteTemplate } = useTemplateContext()
const [selected, setSelected] = useState<Set<string>>(new Set())
const removeSelected = () => {
selected.forEach(deleteTemplate)
setSelected(new Set())
}
return (
<main className="mx-auto max-w-7xl p-6">
<header className="mb-6 flex items-center justify-between">
<h1 className="text-2xl font-bold">Шаблоны протоколов</h1>
<div className="flex gap-3">
{!!selected.size && (
<Button variant="destructive" onClick={removeSelected}>
<Trash2 className="mr-2 h-4 w-4" />
Удалить ({selected.size})
</Button>
)}
<Button onClick={() => navigate('/templates/new')}>
<Plus className="mr-2 h-4 w-4" /> Создать
</Button>
</div>
</header>
<section className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{templates.map(t => (
<TemplateCard
key={t.id}
template={t}
isSelected={selected.has(t.id)}
/>
))}
</section>
</main>
)
}

View File

@@ -1,5 +1,5 @@
import { ElementsCreation } from './ElementsCreation' import { ElementsCreation } from './ElementsCreation'
import { ProtocolCreation } from './ProtocolCreation' import { ProtocolCreation } from './ProtocolCreation'
import { TemplateSetup } from './TemplateSetup' import { TemplateSetup } from './editTemplate'
export { ElementsCreation, ProtocolCreation, TemplateSetup } export { ElementsCreation, ProtocolCreation, TemplateSetup }

View File

@@ -4,17 +4,17 @@ declare module 'app/*' {
export default content export default content
} }
declare module 'pages/*' { declare module 'page/*' {
const content: any const content: any
export default content export default content
} }
declare module 'features/*' { declare module 'feature/*' {
const content: any const content: any
export default content export default content
} }
declare module 'entities/*' { declare module 'entitiy/*' {
const content: any const content: any
export default content export default content
} }

View File

@@ -1,10 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"noImplicitAny": true, "noImplicitAny": true,
"target": "ES2015",
"module": "ESNext", "module": "ESNext",
"target": "ESNext",
"jsx": "react-jsx", "jsx": "react-jsx",
"allowJs": true, "allowJs": false,
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
@@ -15,8 +15,10 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} },
"skipLibCheck": true
}, },
"ts-node": { "ts-node": {
"compilerOptions": { "compilerOptions": {
"module": "CommonJS" "module": "CommonJS"