сетка точками и выделение

This commit is contained in:
2025-07-18 08:35:36 +03:00
parent 7189266bdb
commit fda99279d3
4 changed files with 540 additions and 400 deletions

View File

@@ -1,58 +1,91 @@
import { Calendar, LayoutGrid, Plus, Settings, Trash2 } from 'lucide-react'; import { Calendar, LayoutGrid, Plus, Settings, Trash2 } from 'lucide-react'
import React, { useState } from 'react'; import React, { useState } from 'react'
import { getAvailableElementTypes, getElementDefinition } from '../../lib/element-registry'; import {
import { autoArrangeElements, findFreePosition, generateDefaultLayout } from '../../lib/utils'; getAvailableElementTypes,
import { CellTarget, ElementType, FormLayoutSettings, TemplateElement } from '../../types/template'; getElementDefinition,
import { Button } from '../ui/button'; } from '../../lib/element-registry'
import { Checkbox } from '../ui/checkbox'; import {
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog'; autoArrangeElements,
import { Input } from '../ui/input'; findFreePosition,
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; generateDefaultLayout,
import { Textarea } from '../ui/textarea'; } from '../../lib/utils'
import { VisualLayoutEditor } from './VisualLayoutEditor'; import {
CellTarget,
ElementType,
FormLayoutSettings,
TemplateElement,
} from '../../types/template'
import { Button } from '../ui/button'
import { Checkbox } from '../ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '../ui/dialog'
import { Input } from '../ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../ui/select'
import { Textarea } from '../ui/textarea'
import { VisualLayoutEditor } from './VisualLayoutEditor'
interface ElementFormProps { interface ElementFormProps {
formData: Partial<TemplateElement>; formData: Partial<TemplateElement>
setFormData: React.Dispatch<React.SetStateAction<Partial<TemplateElement>>>; setFormData: React.Dispatch<React.SetStateAction<Partial<TemplateElement>>>
} }
const CellTargetEditor: React.FC<{ const CellTargetEditor: React.FC<{
targets: CellTarget[]; targets: CellTarget[]
onChange: (targets: CellTarget[]) => void; onChange: (targets: CellTarget[]) => void
}> = ({ targets, onChange }) => { }> = ({ targets, onChange }) => {
const handleAddTarget = () => { const handleAddTarget = () => {
onChange([...targets, { sheet: 'L', cell: '', displayName: '' }]); onChange([...targets, { sheet: 'L', cell: '', displayName: '' }])
}; }
const handleUpdateTarget = (index: number, field: keyof CellTarget, value: string) => { const handleUpdateTarget = (
index: number,
field: keyof CellTarget,
value: string,
) => {
const updatedTargets = targets.map((target, i) => const updatedTargets = targets.map((target, i) =>
i === index ? { ...target, [field]: value } : target i === index ? { ...target, [field]: value } : target,
); )
onChange(updatedTargets); onChange(updatedTargets)
}; }
const handleRemoveTarget = (index: number) => { const handleRemoveTarget = (index: number) => {
onChange(targets.filter((_, i) => i !== index)); onChange(targets.filter((_, i) => i !== index))
}; }
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-sm font-medium">Целевые ячейки</label> <label className="text-sm font-medium">Целевые ячейки</label>
<Button variant="outline" size="sm" onClick={handleAddTarget}> <Button variant="outline" size="sm" onClick={handleAddTarget}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="mr-1 h-4 w-4" />
Добавить ячейку Добавить ячейку
</Button> </Button>
</div> </div>
<div className="space-y-3 max-h-48 overflow-y-auto"> <div className="max-h-48 space-y-3 overflow-y-auto">
{targets.map((target, index) => ( {targets.map((target, index) => (
<div key={index} className="p-3 border rounded-lg bg-gray-50"> <div key={index} className="rounded-lg border bg-gray-50 p-3">
<div className="grid grid-cols-3 gap-2 mb-2"> <div className="mb-2 grid grid-cols-3 gap-2">
<div className="space-y-1"> <div className="space-y-1">
<label className="text-xs font-medium text-gray-600">Лист</label> <label className="text-xs font-medium text-gray-600">
Лист
</label>
<Select <Select
value={target.sheet} value={target.sheet}
onValueChange={(value) => handleUpdateTarget(index, 'sheet', value)} onValueChange={(value) =>
handleUpdateTarget(index, 'sheet', value)
}
> >
<SelectTrigger className="h-8"> <SelectTrigger className="h-8">
<SelectValue /> <SelectValue />
@@ -66,20 +99,28 @@ const CellTargetEditor: React.FC<{
</Select> </Select>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-xs font-medium text-gray-600">Ячейка</label> <label className="text-xs font-medium text-gray-600">
Ячейка
</label>
<Input <Input
placeholder="A1, B5..." placeholder="A1, B5..."
value={target.cell} value={target.cell}
onChange={(e) => handleUpdateTarget(index, 'cell', e.target.value)} onChange={(e) =>
handleUpdateTarget(index, 'cell', e.target.value)
}
className="h-8" className="h-8"
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-xs font-medium text-gray-600">Название</label> <label className="text-xs font-medium text-gray-600">
Название
</label>
<Input <Input
placeholder="Описание" placeholder="Описание"
value={target.displayName || ''} value={target.displayName || ''}
onChange={(e) => handleUpdateTarget(index, 'displayName', e.target.value)} onChange={(e) =>
handleUpdateTarget(index, 'displayName', e.target.value)
}
className="h-8" className="h-8"
/> />
</div> </div>
@@ -90,27 +131,31 @@ const CellTargetEditor: React.FC<{
onClick={() => handleRemoveTarget(index)} onClick={() => handleRemoveTarget(index)}
className="h-6 text-xs text-red-600 hover:text-red-700" className="h-6 text-xs text-red-600 hover:text-red-700"
> >
<Trash2 className="h-3 w-3 mr-1" /> <Trash2 className="mr-1 h-3 w-3" />
Удалить Удалить
</Button> </Button>
</div> </div>
))} ))}
{targets.length === 0 && ( {targets.length === 0 && (
<p className="text-sm text-gray-500 text-center py-4"> <p className="py-4 text-center text-sm text-gray-500">
Добавьте ячейки для связи с элементом Добавьте ячейки для связи с элементом
</p> </p>
)} )}
</div> </div>
</div> </div>
); )
}; }
// Live Preview компонент // Live Preview компонент
const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({ element }) => { const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({
const elementDefinition = element.type ? getElementDefinition(element.type) : null; element,
}) => {
const elementDefinition = element.type
? getElementDefinition(element.type)
: null
if (elementDefinition && elementDefinition.Preview) { if (elementDefinition && elementDefinition.Preview) {
return <elementDefinition.Preview config={element as any} />; return <elementDefinition.Preview config={element as any} />
} }
// Fallback для старых элементов // Fallback для старых элементов
@@ -123,7 +168,7 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({ eleme
disabled disabled
className="w-full" className="w-full"
/> />
); )
case 'textarea': case 'textarea':
return ( return (
@@ -133,7 +178,7 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({ eleme
rows={3} rows={3}
className="w-full resize-none" className="w-full resize-none"
/> />
); )
case 'number': case 'number':
return ( return (
@@ -143,16 +188,10 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({ eleme
disabled disabled
className="w-full" className="w-full"
/> />
); )
case 'date': case 'date':
return ( return <Input type="date" disabled className="w-full" />
<Input
type="date"
disabled
className="w-full"
/>
);
case 'select': case 'select':
return ( return (
@@ -163,39 +202,41 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({ eleme
</span> </span>
</SelectTrigger> </SelectTrigger>
</Select> </Select>
); )
case 'radio': case 'radio':
return ( return (
<div className="space-y-2"> <div className="space-y-2">
{element.options?.length ? element.options.map((option, index) => ( {element.options?.length ? (
element.options.map((option, index) => (
<div key={index} className="flex items-center space-x-2"> <div key={index} className="flex items-center space-x-2">
<div className="w-4 h-4 border border-gray-300 rounded-full" /> <div className="h-4 w-4 rounded-full border border-gray-300" />
<span className="text-sm">{option.label}</span> <span className="text-sm">{option.label}</span>
</div> </div>
)) : ( ))
) : (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="w-4 h-4 border border-gray-300 rounded-full" /> <div className="h-4 w-4 rounded-full border border-gray-300" />
<span className="text-sm text-gray-500">Вариант 1</span> <span className="text-sm text-gray-500">Вариант 1</span>
</div> </div>
)} )}
</div> </div>
); )
case 'checkbox': case 'checkbox':
return ( return (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="w-4 h-4 border border-gray-300 rounded" /> <div className="h-4 w-4 rounded border border-gray-300" />
<span className="text-sm"> <span className="text-sm">
{element.placeholder || element.label || 'Отметить'} {element.placeholder || element.label || 'Отметить'}
</span> </span>
</div> </div>
); )
case 'standards': case 'standards':
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between p-3 border border-input rounded-md bg-background"> <div className="flex items-center justify-between rounded-md border border-input bg-background p-3">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{element.placeholder || 'Выберите эталоны'} {element.placeholder || 'Выберите эталоны'}
</span> </span>
@@ -208,34 +249,34 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({ eleme
Выбрано эталонов: 0 Выбрано эталонов: 0
</div> </div>
</div> </div>
); )
default: default:
return ( return (
<div className="p-3 border border-gray-200 rounded bg-gray-50 text-center text-sm text-gray-500"> <div className="rounded border border-gray-200 bg-gray-50 p-3 text-center text-sm text-gray-500">
Выберите тип элемента Выберите тип элемента
</div> </div>
); )
}
} }
};
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">
{element.label && ( {element.label && (
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="mb-2 block text-sm font-medium text-gray-700">
{element.label} {element.label}
{element.required && <span className="text-red-500 ml-1">*</span>} {element.required && <span className="ml-1 text-red-500">*</span>}
</label> </label>
)} )}
{renderPreviewInput()} {renderPreviewInput()}
{element.targetCells && element.targetCells.length > 0 && ( {element.targetCells && element.targetCells.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2"> <div className="mt-2 flex flex-wrap gap-1">
{element.targetCells.map((target, index) => ( {element.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}
@@ -245,34 +286,42 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({ eleme
)} )}
</div> </div>
</div> </div>
); )
}; }
const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => { const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
const elementDefinition = formData.type ? getElementDefinition(formData.type) : null; const elementDefinition = formData.type
? getElementDefinition(formData.type)
: null
const handleAddOption = () => { const handleAddOption = () => {
setFormData(prev => ({ setFormData((prev) => ({
...prev, ...prev,
options: [...(prev.options || []), { value: '', label: '' }], options: [...(prev.options || []), { value: '', label: '' }],
})); }))
}; }
const handleUpdateOption = (index: number, field: 'value' | 'label', value: string) => { const handleUpdateOption = (
setFormData(prev => ({ index: number,
field: 'value' | 'label',
value: string,
) => {
setFormData((prev) => ({
...prev, ...prev,
options: prev.options?.map((option, i) => (i === index ? { ...option, [field]: value } : option)), options: prev.options?.map((option, i) =>
})); i === index ? { ...option, [field]: value } : option,
}; ),
}))
}
const handleRemoveOption = (index: number) => { const handleRemoveOption = (index: number) => {
setFormData(prev => ({ setFormData((prev) => ({
...prev, ...prev,
options: prev.options?.filter((_, i) => i !== index), options: prev.options?.filter((_, i) => i !== index),
})); }))
}; }
const needsOptions = formData.type === 'select' || formData.type === 'radio'; const needsOptions = formData.type === 'select' || formData.type === 'radio'
// Если есть определение элемента в реестре, используем его редактор // Если есть определение элемента в реестре, используем его редактор
if (elementDefinition && elementDefinition.Editor) { if (elementDefinition && elementDefinition.Editor) {
@@ -284,31 +333,35 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
<label className="text-sm font-medium">Тип элемента</label> <label className="text-sm font-medium">Тип элемента</label>
<Select <Select
value={formData.type} value={formData.type}
onValueChange={value => { onValueChange={(value) => {
const newType = value as ElementType; const newType = value as ElementType
console.log('Element type changed to:', newType); console.log('Element type changed to:', newType)
setFormData(prev => { setFormData((prev) => {
const updates: Partial<TemplateElement> = { type: newType }; const updates: Partial<TemplateElement> = { type: newType }
// Если выбран тип "standards", устанавливаем значения по умолчанию // Если выбран тип "standards", устанавливаем значения по умолчанию
if (newType === 'standards') { if (newType === 'standards') {
console.log('Standards type selected, getting element definition'); console.log(
const elementDefinition = getElementDefinition('standards'); 'Standards type selected, getting element definition',
console.log('Element definition:', elementDefinition); )
const elementDefinition = getElementDefinition('standards')
console.log('Element definition:', elementDefinition)
if (elementDefinition && elementDefinition.defaultConfig) { if (elementDefinition && elementDefinition.defaultConfig) {
const defaultConfig = elementDefinition.defaultConfig as any; const defaultConfig =
console.log('Default config:', defaultConfig); elementDefinition.defaultConfig as any
updates.targetCells = elementDefinition.mapToCells?.(defaultConfig) || []; console.log('Default config:', defaultConfig)
console.log('Generated targetCells:', updates.targetCells); updates.targetCells =
elementDefinition.mapToCells?.(defaultConfig) || []
console.log('Generated targetCells:', updates.targetCells)
} }
} }
const newFormData = { ...prev, ...updates }; const newFormData = { ...prev, ...updates }
console.log('New form data:', newFormData); console.log('New form data:', newFormData)
return newFormData; return newFormData
}); })
}} }}
> >
<SelectTrigger> <SelectTrigger>
@@ -333,7 +386,9 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
<Input <Input
placeholder="field_name" placeholder="field_name"
value={formData.name} value={formData.name}
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))} onChange={(e) =>
setFormData((prev) => ({ ...prev, name: e.target.value }))
}
/> />
</div> </div>
@@ -342,20 +397,26 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
<Input <Input
placeholder="Название поля" placeholder="Название поля"
value={formData.label} value={formData.label}
onChange={e => setFormData(prev => ({ ...prev, label: e.target.value }))} onChange={(e) =>
setFormData((prev) => ({ ...prev, label: e.target.value }))
}
/> />
</div> </div>
</div> </div>
<CellTargetEditor <CellTargetEditor
targets={formData.targetCells || []} targets={formData.targetCells || []}
onChange={(targets) => setFormData(prev => ({ ...prev, targetCells: targets }))} onChange={(targets) =>
setFormData((prev) => ({ ...prev, targetCells: targets }))
}
/> />
{/* Специфичный редактор элемента */} {/* Специфичный редактор элемента */}
<elementDefinition.Editor <elementDefinition.Editor
config={formData as any} config={formData as any}
onChange={(newConfig) => setFormData(prev => ({ ...prev, ...newConfig }))} onChange={(newConfig) =>
setFormData((prev) => ({ ...prev, ...newConfig }))
}
/> />
</div> </div>
@@ -364,7 +425,7 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
<ElementPreview element={formData} /> <ElementPreview element={formData} />
</div> </div>
</div> </div>
); )
} }
// Fallback для старых элементов // Fallback для старых элементов
@@ -376,31 +437,40 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
<label className="text-sm font-medium">Тип элемента</label> <label className="text-sm font-medium">Тип элемента</label>
<Select <Select
value={formData.type} value={formData.type}
onValueChange={value => { onValueChange={(value) => {
const newType = value as ElementType; const newType = value as ElementType
console.log('Element type changed to (fallback):', newType); console.log('Element type changed to (fallback):', newType)
setFormData(prev => { setFormData((prev) => {
const updates: Partial<TemplateElement> = { type: newType }; const updates: Partial<TemplateElement> = { type: newType }
// Если выбран тип "standards", устанавливаем значения по умолчанию // Если выбран тип "standards", устанавливаем значения по умолчанию
if (newType === 'standards') { if (newType === 'standards') {
console.log('Standards type selected (fallback), getting element definition'); console.log(
const elementDefinition = getElementDefinition('standards'); 'Standards type selected (fallback), getting element definition',
console.log('Element definition (fallback):', elementDefinition); )
const elementDefinition = getElementDefinition('standards')
console.log(
'Element definition (fallback):',
elementDefinition,
)
if (elementDefinition && elementDefinition.defaultConfig) { if (elementDefinition && elementDefinition.defaultConfig) {
const defaultConfig = elementDefinition.defaultConfig as any; const defaultConfig = elementDefinition.defaultConfig as any
console.log('Default config (fallback):', defaultConfig); console.log('Default config (fallback):', defaultConfig)
updates.targetCells = elementDefinition.mapToCells?.(defaultConfig) || []; updates.targetCells =
console.log('Generated targetCells (fallback):', updates.targetCells); elementDefinition.mapToCells?.(defaultConfig) || []
console.log(
'Generated targetCells (fallback):',
updates.targetCells,
)
} }
} }
const newFormData = { ...prev, ...updates }; const newFormData = { ...prev, ...updates }
console.log('New form data (fallback):', newFormData); console.log('New form data (fallback):', newFormData)
return newFormData; return newFormData
}); })
}} }}
> >
<SelectTrigger> <SelectTrigger>
@@ -425,7 +495,9 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
<Input <Input
placeholder="field_name" placeholder="field_name"
value={formData.name} value={formData.name}
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))} onChange={(e) =>
setFormData((prev) => ({ ...prev, name: e.target.value }))
}
/> />
</div> </div>
@@ -434,7 +506,9 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
<Input <Input
placeholder="Название поля" placeholder="Название поля"
value={formData.label} value={formData.label}
onChange={e => setFormData(prev => ({ ...prev, label: e.target.value }))} onChange={(e) =>
setFormData((prev) => ({ ...prev, label: e.target.value }))
}
/> />
</div> </div>
</div> </div>
@@ -444,7 +518,9 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
<Input <Input
placeholder="Введите значение..." placeholder="Введите значение..."
value={formData.placeholder} value={formData.placeholder}
onChange={e => setFormData(prev => ({ ...prev, placeholder: e.target.value }))} onChange={(e) =>
setFormData((prev) => ({ ...prev, placeholder: e.target.value }))
}
/> />
</div> </div>
@@ -452,7 +528,9 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
<Checkbox <Checkbox
id="required" id="required"
checked={formData.required} checked={formData.required}
onCheckedChange={checked => setFormData(prev => ({ ...prev, required: !!checked }))} onCheckedChange={(checked) =>
setFormData((prev) => ({ ...prev, required: !!checked }))
}
/> />
<label htmlFor="required" className="text-sm font-medium"> <label htmlFor="required" className="text-sm font-medium">
Обязательное поле Обязательное поле
@@ -461,7 +539,9 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
<CellTargetEditor <CellTargetEditor
targets={formData.targetCells || []} targets={formData.targetCells || []}
onChange={(targets) => setFormData(prev => ({ ...prev, targetCells: targets }))} onChange={(targets) =>
setFormData((prev) => ({ ...prev, targetCells: targets }))
}
/> />
{needsOptions && ( {needsOptions && (
@@ -469,24 +549,32 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-sm font-medium">Варианты</label> <label className="text-sm font-medium">Варианты</label>
<Button variant="outline" size="sm" onClick={handleAddOption}> <Button variant="outline" size="sm" onClick={handleAddOption}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="mr-1 h-4 w-4" />
Добавить Добавить
</Button> </Button>
</div> </div>
<div className="space-y-2 max-h-32 overflow-y-auto"> <div className="max-h-32 space-y-2 overflow-y-auto">
{formData.options?.map((option, index) => ( {formData.options?.map((option, index) => (
<div key={index} className="flex gap-2"> <div key={index} className="flex gap-2">
<Input <Input
placeholder="Значение" placeholder="Значение"
value={option.value} value={option.value}
onChange={e => handleUpdateOption(index, 'value', e.target.value)} onChange={(e) =>
handleUpdateOption(index, 'value', e.target.value)
}
/> />
<Input <Input
placeholder="Подпись" placeholder="Подпись"
value={option.label} value={option.label}
onChange={e => handleUpdateOption(index, 'label', e.target.value)} onChange={(e) =>
handleUpdateOption(index, 'label', e.target.value)
}
/> />
<Button variant="outline" size="icon" onClick={() => handleRemoveOption(index)}> <Button
variant="outline"
size="icon"
onClick={() => handleRemoveOption(index)}
>
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
</div> </div>
@@ -501,22 +589,27 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
<ElementPreview element={formData} /> <ElementPreview element={formData} />
</div> </div>
</div> </div>
); )
};
interface CanvasSettingsProps {
settings: FormLayoutSettings;
onChange: (settings: FormLayoutSettings) => void;
} }
const CanvasSettings: React.FC<CanvasSettingsProps> = ({ settings, onChange }) => { interface CanvasSettingsProps {
settings: FormLayoutSettings
onChange: (settings: FormLayoutSettings) => void
}
const CanvasSettings: React.FC<CanvasSettingsProps> = ({
settings,
onChange,
}) => {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Размер сетки (px)</label> <label className="text-sm font-medium">Размер сетки (px)</label>
<Select <Select
value={settings.gridSize.toString()} value={settings.gridSize.toString()}
onValueChange={(value) => onChange({ ...settings, gridSize: parseInt(value) })} onValueChange={(value) =>
onChange({ ...settings, gridSize: parseInt(value) })
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@@ -534,24 +627,29 @@ const CanvasSettings: React.FC<CanvasSettingsProps> = ({ settings, onChange }) =
<Checkbox <Checkbox
id="showGrid" id="showGrid"
checked={settings.showGrid} checked={settings.showGrid}
onCheckedChange={(checked) => onChange({ ...settings, showGrid: !!checked })} onCheckedChange={(checked) =>
onChange({ ...settings, showGrid: !!checked })
}
/> />
<label htmlFor="showGrid" className="text-sm font-medium"> <label htmlFor="showGrid" className="text-sm font-medium">
Показывать сетку Показывать сетку
</label> </label>
</div> </div>
</div> </div>
); )
}; }
interface ElementConstructorProps { interface ElementConstructorProps {
elements: TemplateElement[]; elements: TemplateElement[]
layoutSettings: FormLayoutSettings; layoutSettings: FormLayoutSettings
onElementAdd: (element: TemplateElement) => void; onElementAdd: (element: TemplateElement) => void
onElementUpdate: (elementId: string, updates: Partial<TemplateElement>) => void; onElementUpdate: (
onElementDelete: (elementId: string) => void; elementId: string,
onElementsReorder: (elements: TemplateElement[]) => void; updates: Partial<TemplateElement>,
onLayoutSettingsChange: (settings: FormLayoutSettings) => void; ) => void
onElementDelete: (elementId: string) => void
onElementsReorder: (elements: TemplateElement[]) => void
onLayoutSettingsChange: (settings: FormLayoutSettings) => void
} }
export const ElementConstructor: React.FC<ElementConstructorProps> = ({ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
@@ -563,9 +661,11 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
// onElementsReorder, // onElementsReorder,
onLayoutSettingsChange, onLayoutSettingsChange,
}) => { }) => {
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false)
const [editingElement, setEditingElement] = useState<TemplateElement | null>(null); const [editingElement, setEditingElement] = useState<TemplateElement | null>(
null,
)
const [formData, setFormData] = useState<Partial<TemplateElement>>({ const [formData, setFormData] = useState<Partial<TemplateElement>>({
type: 'text', type: 'text',
name: '', name: '',
@@ -574,7 +674,7 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
placeholder: '', placeholder: '',
required: false, required: false,
options: [], options: [],
}); })
const resetForm = () => { const resetForm = () => {
setFormData({ setFormData({
@@ -585,42 +685,44 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
placeholder: '', placeholder: '',
required: false, required: false,
options: [], options: [],
}); })
}; }
const handleAddElement = () => { const handleAddElement = () => {
console.log('handleAddElement called with formData:', formData); console.log('handleAddElement called with formData:', formData)
if (!formData.name || !formData.label) { if (!formData.name || !formData.label) {
console.log('Validation failed: missing name or label'); console.log('Validation failed: missing name or label')
return; return
} }
// Для элемента типа "standards" не требуем targetCells, так как они генерируются автоматически // Для элемента типа "standards" не требуем targetCells, так как они генерируются автоматически
if (formData.type !== 'standards' && !formData.targetCells?.length) { if (formData.type !== 'standards' && !formData.targetCells?.length) {
console.log('Validation failed: missing targetCells for non-standards element'); console.log(
return; 'Validation failed: missing targetCells for non-standards element',
)
return
} }
// Генерируем layout для нового элемента // Генерируем layout для нового элемента
const existingLayouts = elements.map(e => e.layout).filter(Boolean); const existingLayouts = elements.map((e) => e.layout).filter(Boolean)
const defaultLayout = generateDefaultLayout(elements.length); const defaultLayout = generateDefaultLayout(elements.length)
const layout = findFreePosition( const layout = findFreePosition(
defaultLayout, defaultLayout,
existingLayouts as any[], existingLayouts as any[],
layoutSettings.gridSize layoutSettings.gridSize,
); )
// Специальная обработка для эталонов // Специальная обработка для эталонов
let targetCells = formData.targetCells || []; let targetCells = formData.targetCells || []
if (formData.type === 'standards') { if (formData.type === 'standards') {
// Для эталонов генерируем targetCells автоматически, если их нет // Для эталонов генерируем targetCells автоматически, если их нет
if (!targetCells.length) { if (!targetCells.length) {
const elementDefinition = getElementDefinition('standards'); const elementDefinition = getElementDefinition('standards')
if (elementDefinition && elementDefinition.defaultConfig) { if (elementDefinition && elementDefinition.defaultConfig) {
const defaultConfig = elementDefinition.defaultConfig as any; const defaultConfig = elementDefinition.defaultConfig as any
targetCells = elementDefinition.mapToCells?.(defaultConfig) || []; targetCells = elementDefinition.mapToCells?.(defaultConfig) || []
} }
} }
} }
@@ -636,75 +738,76 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
options: formData.options, options: formData.options,
order: elements.length, order: elements.length,
layout, layout,
}; }
onElementAdd(newElement); onElementAdd(newElement)
resetForm(); resetForm()
setIsAddDialogOpen(false); setIsAddDialogOpen(false)
}; }
const handleEditElement = (element: TemplateElement) => { const handleEditElement = (element: TemplateElement) => {
setEditingElement(element); setEditingElement(element)
setFormData(element); setFormData(element)
}; }
const handleUpdateElement = () => { const handleUpdateElement = () => {
if (!editingElement || !formData.name || !formData.label) return; if (!editingElement || !formData.name || !formData.label) return
// Для элемента типа "standards" не требуем targetCells, так как они генерируются автоматически // Для элемента типа "standards" не требуем targetCells, так как они генерируются автоматически
if (formData.type !== 'standards' && !formData.targetCells?.length) return; if (formData.type !== 'standards' && !formData.targetCells?.length) return
onElementUpdate(editingElement.id, formData); onElementUpdate(editingElement.id, formData)
setEditingElement(null); setEditingElement(null)
resetForm(); resetForm()
}; }
const handleCancelEdit = () => { const handleCancelEdit = () => {
setEditingElement(null); setEditingElement(null)
resetForm(); resetForm()
}; }
const handleAutoArrange = () => { const handleAutoArrange = () => {
const arrangedElements = autoArrangeElements( const arrangedElements = autoArrangeElements(
elements, elements,
layoutSettings.gridSize layoutSettings.gridSize,
); )
arrangedElements.forEach((element, index) => { arrangedElements.forEach((element, index) => {
onElementUpdate(element.id, { layout: element.layout, order: index }); onElementUpdate(element.id, { layout: element.layout, order: index })
}); })
}; }
const handleDeleteAndSelect = (elementId: string) => { const handleDeleteAndSelect = (elementId: string) => {
onElementDelete(elementId); onElementDelete(elementId)
setEditingElement(null); setEditingElement(null)
resetForm(); resetForm()
}; }
// const reordered = (reorderedElements: TemplateElement[]) => { // const reordered = (reorderedElements: TemplateElement[]) => {
// onElementsReorder(reorderedElements); // onElementsReorder(reorderedElements);
// onLayoutSettingsChange({ ...layoutSettings, showGrid: true }); // onLayoutSettingsChange({ ...layoutSettings, showGrid: true });
// }; // };
return ( return (
<div className="flex h-full w-full"> <div className="flex h-full w-full">
{/* Main Content */} {/* Main Content */}
<div className="flex-1 flex flex-col bg-gray-50"> <div className="flex flex-1 flex-col bg-gray-50">
<div className="flex items-center justify-between p-2 border-b bg-white"> <div className="flex items-center justify-between border-b bg-white p-2">
<div className='px-2'> <div className="px-2">
<h3 className="text-sm font-medium">Холст</h3> <h3 className="text-sm font-medium">Холст</h3>
<p className="text-xs text-gray-500">Перетаскивайте элементы для настройки их расположения</p> <p className="text-xs text-gray-500">
Перетаскивайте элементы для настройки их расположения
</p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}> <Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
<Plus className="h-4 w-4 mr-2" /> <Plus className="mr-2 h-4 w-4" />
Добавить элемент Добавить элемент
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto"> <DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Добавить новый элемент</DialogTitle> <DialogTitle>Добавить новый элемент</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -713,14 +816,22 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
</DialogHeader> </DialogHeader>
<ElementForm formData={formData} setFormData={setFormData} /> <ElementForm formData={formData} setFormData={setFormData} />
<div className="flex justify-end gap-2 pt-4"> <div className="flex justify-end gap-2 pt-4">
<Button onClick={() => setIsAddDialogOpen(false)} variant="outline"> <Button
onClick={() => setIsAddDialogOpen(false)}
variant="outline"
>
Отмена Отмена
</Button> </Button>
<Button <Button
onClick={handleAddElement} onClick={handleAddElement}
disabled={!formData.name || !formData.label || (formData.type !== 'standards' && !formData.targetCells?.length)} disabled={
!formData.name ||
!formData.label ||
(formData.type !== 'standards' &&
!formData.targetCells?.length)
}
> >
<Plus className="h-4 w-4 mr-2" /> <Plus className="mr-2 h-4 w-4" />
Добавить элемент Добавить элемент
</Button> </Button>
</div> </div>
@@ -728,8 +839,11 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
</Dialog> </Dialog>
{editingElement && ( {editingElement && (
<Dialog open={!!editingElement} onOpenChange={() => handleCancelEdit()}> <Dialog
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto"> open={!!editingElement}
onOpenChange={() => handleCancelEdit()}
>
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Редактировать элемент</DialogTitle> <DialogTitle>Редактировать элемент</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -750,13 +864,13 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
)} )}
<Button onClick={handleAutoArrange} variant="outline" size="sm"> <Button onClick={handleAutoArrange} variant="outline" size="sm">
<LayoutGrid className="h-4 w-4 mr-2" /> <LayoutGrid className="mr-2 h-4 w-4" />
Расставить автоматически Расставить автоматически
</Button> </Button>
<Dialog open={isSettingsOpen} onOpenChange={setIsSettingsOpen}> <Dialog open={isSettingsOpen} onOpenChange={setIsSettingsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
<Settings className="h-4 w-4 mr-2" /> <Settings className="mr-2 h-4 w-4" />
Настройки холста Настройки холста
</Button> </Button>
</DialogTrigger> </DialogTrigger>
@@ -767,7 +881,10 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
Настройте отображение сетки и другие параметры. Настройте отображение сетки и другие параметры.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<CanvasSettings settings={layoutSettings} onChange={onLayoutSettingsChange} /> <CanvasSettings
settings={layoutSettings}
onChange={onLayoutSettingsChange}
/>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </div>
@@ -784,5 +901,5 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
</div> </div>
</div> </div>
</div> </div>
); )
}; }

View File

@@ -289,7 +289,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
<div <div
className="absolute inset-0 pointer-events-none" className="absolute inset-0 pointer-events-none"
style={{ style={{
backgroundImage: `linear-gradient(to right, #e5e7eb 1px, transparent 1px), linear-gradient(to bottom, #e5e7eb 1px, transparent 1px)`, backgroundImage: `radial-gradient(circle, #e5e7eb 1px, transparent 1px)`,
backgroundSize: `${gridSize}px ${gridSize}px`, backgroundSize: `${gridSize}px ${gridSize}px`,
}} }}
/> />
@@ -301,6 +301,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
}, [onElementUpdate]); }, [onElementUpdate]);
const handleCanvasClick = useCallback((e: React.MouseEvent) => { const handleCanvasClick = useCallback((e: React.MouseEvent) => {
// Снимаем выделение при клике по пустому месту
if (e.target === e.currentTarget) { if (e.target === e.currentTarget) {
setSelectedElementId(null); setSelectedElementId(null);
} }
@@ -324,6 +325,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
<div <div
className="relative bg-white shadow-lg mx-auto my-8" className="relative bg-white shadow-lg mx-auto my-8"
style={{ width: canvasSize.width, height: canvasSize.height }} style={{ width: canvasSize.width, height: canvasSize.height }}
onClick={handleCanvasClick}
> >
{GridPattern} {GridPattern}
{elements.map((element) => ( {elements.map((element) => (

View File

@@ -1,116 +1,131 @@
import { Plus, Wrench } from 'lucide-react'; import { Plus, Wrench } from 'lucide-react'
import { FC, useState } from 'react'; import { FC, useState } from 'react'
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom'
import { ElementConstructor } from '../../../../components/TemplateManager/ElementConstructor'; import { ElementConstructor } from '../../../../components/TemplateManager/ElementConstructor'
import { Button } from '../../../../components/ui/button'; import { Button } from '../../../../components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../../../components/ui/card'; import {
import { useTemplateContext } from '../../../../contexts/TemplateContext'; Card,
import { FormLayoutSettings, Template, TemplateElement } from '../../../../types/template'; CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '../../../../components/ui/card'
import { useTemplateContext } from '../../../../contexts/TemplateContext'
import {
FormLayoutSettings,
Template,
TemplateElement,
} from '../../../../types/template'
export const ElementsCreation: FC = () => { export const ElementsCreation: FC = () => {
const navigate = useNavigate(); const navigate = useNavigate()
const { templates, updateTemplate } = useTemplateContext(); const { templates, updateTemplate } = useTemplateContext()
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null); const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
null,
)
const handleTemplateSelect = (template: Template) => { const handleTemplateSelect = (template: Template) => {
setSelectedTemplate(template); setSelectedTemplate(template)
}; }
const handleElementAdd = (element: TemplateElement) => { const handleElementAdd = (element: TemplateElement) => {
console.log('handleElementAdd called with element:', element); console.log('handleElementAdd called with element:', element)
if (!selectedTemplate) { if (!selectedTemplate) {
console.log('No selected template'); console.log('No selected template')
return; return
} }
const updatedTemplate = { const updatedTemplate = {
...selectedTemplate, ...selectedTemplate,
elements: [...selectedTemplate.elements, element], elements: [...selectedTemplate.elements, element],
updatedAt: new Date(), updatedAt: new Date(),
}; }
console.log('Updated template:', updatedTemplate); console.log('Updated template:', updatedTemplate)
console.log('Elements count:', updatedTemplate.elements.length); console.log('Elements count:', updatedTemplate.elements.length)
updateTemplate(updatedTemplate); updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate); setSelectedTemplate(updatedTemplate)
}; }
const handleElementUpdate = (elementId: string, updates: Partial<TemplateElement>) => { const handleElementUpdate = (
if (!selectedTemplate) return; elementId: string,
updates: Partial<TemplateElement>,
) => {
if (!selectedTemplate) return
const updatedTemplate = { const updatedTemplate = {
...selectedTemplate, ...selectedTemplate,
elements: selectedTemplate.elements.map(el => elements: selectedTemplate.elements.map((el) =>
el.id === elementId ? { ...el, ...updates } : el el.id === elementId ? { ...el, ...updates } : el,
), ),
updatedAt: new Date(), updatedAt: new Date(),
}; }
updateTemplate(updatedTemplate); updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate); setSelectedTemplate(updatedTemplate)
}; }
const handleElementDelete = (elementId: string) => { const handleElementDelete = (elementId: string) => {
if (!selectedTemplate) return; if (!selectedTemplate) return
const updatedTemplate = { const updatedTemplate = {
...selectedTemplate, ...selectedTemplate,
elements: selectedTemplate.elements.filter(el => el.id !== elementId), elements: selectedTemplate.elements.filter((el) => el.id !== elementId),
updatedAt: new Date(), updatedAt: new Date(),
}; }
updateTemplate(updatedTemplate); updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate); setSelectedTemplate(updatedTemplate)
}; }
const handleElementsReorder = (reorderedElements: TemplateElement[]) => { const handleElementsReorder = (reorderedElements: TemplateElement[]) => {
if (!selectedTemplate) return; if (!selectedTemplate) return
// Обновляем порядок элементов // Обновляем порядок элементов
const elementsWithOrder = reorderedElements.map((element, index) => ({ const elementsWithOrder = reorderedElements.map((element, index) => ({
...element, ...element,
order: index, order: index,
})); }))
const updatedTemplate = { const updatedTemplate = {
...selectedTemplate, ...selectedTemplate,
elements: elementsWithOrder, elements: elementsWithOrder,
updatedAt: new Date(), updatedAt: new Date(),
}; }
updateTemplate(updatedTemplate); updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate); setSelectedTemplate(updatedTemplate)
}; }
const handleLayoutSettingsChange = (settings: FormLayoutSettings) => { const handleLayoutSettingsChange = (settings: FormLayoutSettings) => {
if (!selectedTemplate) return; if (!selectedTemplate) return
const updatedTemplate = { const updatedTemplate = {
...selectedTemplate, ...selectedTemplate,
layoutSettings: settings, layoutSettings: settings,
updatedAt: new Date(), updatedAt: new Date(),
}; }
updateTemplate(updatedTemplate); updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate); setSelectedTemplate(updatedTemplate)
}; }
const handleBack = () => { const handleBack = () => {
setSelectedTemplate(null); setSelectedTemplate(null)
}; }
// Настройки отображения по умолчанию // Настройки отображения по умолчанию
const defaultLayoutSettings: FormLayoutSettings = { const defaultLayoutSettings: FormLayoutSettings = {
gridSize: 20, gridSize: 20,
showGrid: true, showGrid: true,
}; }
if (selectedTemplate) { if (selectedTemplate) {
return ( return (
<div className="h-full"> <div className="h-full">
<div className="bg-white border-b border-gray-200 p-4 sticky top-0 z-50"> <div className="sticky top-0 z-50 border-b border-gray-200 bg-white p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="ghost" onClick={handleBack}> <Button variant="ghost" onClick={handleBack}>
@@ -120,26 +135,21 @@ export const ElementsCreation: FC = () => {
<h1 className="text-xl font-semibold text-gray-800"> <h1 className="text-xl font-semibold text-gray-800">
Конструктор элементов: {selectedTemplate.name} Конструктор элементов: {selectedTemplate.name}
</h1> </h1>
<p className="text-sm text-gray-600 mt-1"> <p className="mt-1 text-sm text-gray-600">
Создайте элементы формы и разместите их на холсте перетаскиванием Создайте элементы формы и разместите их на холсте
перетаскиванием
</p> </p>
</div> </div>
</div> </div>
<div className="flex gap-2">
<Button onClick={() => navigate('/create-protocol')} variant="outline">
Предпросмотр формы
</Button>
<Button onClick={() => navigate('/templates')} variant="outline">
К шаблонам
</Button>
</div>
</div> </div>
</div> </div>
<div className="h-[calc(100vh-104px)]"> <div className="h-[calc(100vh-104px)]">
<ElementConstructor <ElementConstructor
elements={selectedTemplate.elements} elements={selectedTemplate.elements}
layoutSettings={selectedTemplate.layoutSettings || defaultLayoutSettings} layoutSettings={
selectedTemplate.layoutSettings || defaultLayoutSettings
}
onElementAdd={handleElementAdd} onElementAdd={handleElementAdd}
onElementUpdate={handleElementUpdate} onElementUpdate={handleElementUpdate}
onElementDelete={handleElementDelete} onElementDelete={handleElementDelete}
@@ -148,15 +158,19 @@ export const ElementsCreation: FC = () => {
/> />
</div> </div>
</div> </div>
); )
} }
return ( return (
<div className="p-6 max-w-7xl mx-auto"> <div className="mx-auto max-w-7xl p-6">
<div className="flex items-center justify-between mb-6"> <div className="mb-6 flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Создание элементов</h1> <h1 className="text-3xl font-bold text-gray-900">
<p className="text-gray-600 mt-2">Выберите шаблон для создания элементов интерфейса</p> Создание элементов
</h1>
<p className="mt-2 text-gray-600">
Выберите шаблон для создания элементов интерфейса
</p>
</div> </div>
<Button <Button
@@ -170,19 +184,26 @@ export const ElementsCreation: FC = () => {
</div> </div>
{templates.length === 0 ? ( {templates.length === 0 ? (
<div className="text-center py-12"> <div className="py-12 text-center">
<Wrench className="h-16 w-16 text-gray-400 mx-auto mb-4" /> <Wrench className="mx-auto mb-4 h-16 w-16 text-gray-400" />
<h3 className="text-lg font-medium text-gray-900 mb-2">Нет доступных шаблонов</h3> <h3 className="mb-2 text-lg font-medium text-gray-900">
<p className="text-gray-600 mb-6">Создайте шаблон протокола для начала работы с элементами</p> Нет доступных шаблонов
</h3>
<p className="mb-6 text-gray-600">
Создайте шаблон протокола для начала работы с элементами
</p>
<Button onClick={() => navigate('/templates')}> <Button onClick={() => navigate('/templates')}>
<Plus className="h-4 w-4 mr-2" /> <Plus className="mr-2 h-4 w-4" />
Создать шаблон Создать шаблон
</Button> </Button>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{templates.map((template) => ( {templates.map((template) => (
<Card key={template.id} className="hover:shadow-lg transition-shadow cursor-pointer"> <Card
key={template.id}
className="cursor-pointer transition-shadow hover:shadow-lg"
>
<CardHeader> <CardHeader>
<CardTitle className="text-lg">{template.name}</CardTitle> <CardTitle className="text-lg">{template.name}</CardTitle>
{template.description && ( {template.description && (
@@ -197,7 +218,7 @@ export const ElementsCreation: FC = () => {
<Wrench className="h-4 w-4" /> <Wrench className="h-4 w-4" />
<span>{template.elements.length} элементов создано</span> <span>{template.elements.length} элементов создано</span>
</div> </div>
<div className="text-xs text-gray-500 mt-2"> <div className="mt-2 text-xs text-gray-500">
Обновлен: {template.updatedAt.toLocaleDateString('ru-RU')} Обновлен: {template.updatedAt.toLocaleDateString('ru-RU')}
</div> </div>
</div> </div>
@@ -215,5 +236,5 @@ export const ElementsCreation: FC = () => {
</div> </div>
)} )}
</div> </div>
); )
}; }

View File

@@ -387,7 +387,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
{/* Полноэкранная форма */} {/* Полноэкранная форма */}
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<div <div
className="relative bg-white shadow-lg mx-auto my-8" className="relative mx-auto my-8"
style={{ width: canvasSize.width, height: canvasSize.height }} style={{ width: canvasSize.width, height: canvasSize.height }}
> >
{template.elements.map((element) => { {template.elements.map((element) => {