рефакторинг
This commit is contained in:
@@ -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
1
shared/types.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
type UUID = string
|
||||||
@@ -2,22 +2,26 @@ 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 (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<TemplateProvider>
|
<TemplateProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Layout />}>
|
<Route path="/" element={<Layout />}>
|
||||||
<Route index element={<Navigate to="/templates" replace />} />
|
<Route index element={<Navigate to="/templates" replace />} />
|
||||||
<Route path="templates/" element={<TemplateSetup />} />
|
<Route path="templates/" element={<TemplatesOverviewPage />} />
|
||||||
<Route
|
<Route
|
||||||
path="templates/:templateId/edit"
|
path="templates/:templateId/edit"
|
||||||
element={<TemplateSetup />}
|
element={<TemplateEditPage />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="templates/:templateId/elements"
|
path="templates/:templateId/elements"
|
||||||
@@ -30,6 +34,7 @@ const App: FC = () => {
|
|||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</TemplateProvider>
|
</TemplateProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
className={`mt-1 flex h-5 w-5 items-center justify-center rounded-full border-2 transition-colors ${
|
||||||
isSelected
|
isSelected
|
||||||
? "border-primary bg-primary"
|
? 'border-primary bg-primary'
|
||||||
: "border-border"
|
: '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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useTemplateContext } from '@/context/TemplateContext'
|
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
||||||
import {
|
import {
|
||||||
getFileData,
|
getFileData,
|
||||||
getLatestFileForTemplate,
|
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 { 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) {
|
|
||||||
setSelectedTemplate(template)
|
|
||||||
setIsEditMode(true)
|
setIsEditMode(true)
|
||||||
}
|
}
|
||||||
}
|
}, [initialTemplate])
|
||||||
}, [templateId, templates])
|
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
143
src/entitiy/template/model/TemplateContext.tsx
Normal file
143
src/entitiy/template/model/TemplateContext.tsx
Normal 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
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
|
||||||
// Регистрация всех элементов
|
// Регистрация всех элементов
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
17
src/page/TemplateEditPage.tsx
Normal file
17
src/page/TemplateEditPage.tsx
Normal 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)} />
|
||||||
|
}
|
||||||
@@ -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} />
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { TemplateSetup } from './Page'
|
|
||||||
46
src/page/TemplatesOverviewPage.tsx
Normal file
46
src/page/TemplatesOverviewPage.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
|||||||
6
src/type/imports.d.ts
vendored
6
src/type/imports.d.ts
vendored
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user