элементы и передвижение
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/component/ui/select'
|
} from '@/component/ui/select'
|
||||||
import { ElementDefinition } from '@/entitiy/element/model/interface'
|
import { ElementDefinition } from '@/entitiy/element/model/interface'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Check,
|
Check,
|
||||||
@@ -255,8 +256,7 @@ function CalibrationCondInfo({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Компактное отображение */}
|
<div className="flex items-center justify-between gap-3 rounded-lg border bg-muted/50 px-3 py-2">
|
||||||
<div className="flex items-center gap-3 rounded-lg border bg-muted/50 px-3 py-2">
|
|
||||||
<div className="flex items-center gap-3 text-xs">
|
<div className="flex items-center gap-3 text-xs">
|
||||||
{fieldConfigs.map(({ key, icon: Icon, unit }) => (
|
{fieldConfigs.map(({ key, icon: Icon, unit }) => (
|
||||||
<div key={key} className="flex items-center gap-1">
|
<div key={key} className="flex items-center gap-1">
|
||||||
@@ -268,19 +268,24 @@ function CalibrationCondInfo({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
<div className="flex items-center gap-2">
|
||||||
size="icon"
|
{isOutdated && (
|
||||||
onClick={openModal}
|
<div className="h-2 w-2 animate-pulse rounded-full bg-yellow-500" />
|
||||||
className={`h-6 w-6 p-0 ${
|
)}
|
||||||
isOutdated ? 'text-yellow-600 hover:text-yellow-700' : ''
|
|
||||||
}`}
|
<Button
|
||||||
>
|
variant="ghost"
|
||||||
<Edit3 className="h-3 w-3" />
|
size="icon"
|
||||||
</Button>
|
onClick={openModal}
|
||||||
{isOutdated && (
|
className={cn(
|
||||||
<div className="h-2 w-2 animate-pulse rounded-full bg-yellow-500" />
|
'h-6 w-6 p-0',
|
||||||
)}
|
isOutdated && 'text-yellow-600 hover:text-yellow-700'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Edit3 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Модальное окно */}
|
{/* Модальное окно */}
|
||||||
@@ -729,23 +734,20 @@ export const calibrationConditionsDefinition: ElementDefinition<
|
|||||||
},
|
},
|
||||||
Preview: ({ config }) => (
|
Preview: ({ config }) => (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
|
<CalibrationCondInfo
|
||||||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
conditions={
|
||||||
<CalibrationCondInfo
|
config.defaultConditions || {
|
||||||
conditions={
|
temperature: '20',
|
||||||
config.defaultConditions || {
|
humidity: '65',
|
||||||
temperature: '20',
|
pressure: '101.3',
|
||||||
humidity: '65',
|
voltage: '220',
|
||||||
pressure: '101.3',
|
frequency: '50',
|
||||||
voltage: '220',
|
lastUpdated: new Date().toISOString(),
|
||||||
frequency: '50',
|
|
||||||
lastUpdated: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
isOutdated={false}
|
}
|
||||||
onUpdate={async () => true}
|
isOutdated={false}
|
||||||
/>
|
onUpdate={async () => true}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
Render: ({ config, value, onChange }) => {
|
Render: ({ config, value, onChange }) => {
|
||||||
|
|||||||
@@ -773,7 +773,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
const autoSaveOptions = useMemo(
|
const autoSaveOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
templateId,
|
templateId,
|
||||||
autoSaveDelay: 2000,
|
autoSaveDelay: 4000,
|
||||||
enableAutoSave: enableAutoSave && userEdited,
|
enableAutoSave: enableAutoSave && userEdited,
|
||||||
}),
|
}),
|
||||||
[templateId, enableAutoSave, userEdited]
|
[templateId, enableAutoSave, userEdited]
|
||||||
@@ -824,7 +824,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
if (fileData && Object.keys(fileData.data).length > 0) {
|
if (fileData && Object.keys(fileData.data).length > 0) {
|
||||||
finalBaseData = { [SHEET_NAMES.report]: fileData.data }
|
finalBaseData = { [SHEET_NAMES.report]: fileData.data }
|
||||||
setBaseFileData(finalBaseData)
|
setBaseFileData(finalBaseData)
|
||||||
// Сохраняем baseline, чтобы в листы попадали только пользовательские изменения
|
|
||||||
setTemplateData(finalBaseData)
|
setTemplateData(finalBaseData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,7 @@ import {
|
|||||||
getElementDefinition,
|
getElementDefinition,
|
||||||
} from '@/entitiy/element/model/interface'
|
} from '@/entitiy/element/model/interface'
|
||||||
import { useToast } from '@/lib/hooks/useToast'
|
import { useToast } from '@/lib/hooks/useToast'
|
||||||
import {
|
import { generateDefaultLayout } from '@/lib/utils'
|
||||||
autoArrangeElements,
|
|
||||||
findFreePosition,
|
|
||||||
generateDefaultLayout,
|
|
||||||
} from '@/lib/utils'
|
|
||||||
import {
|
import {
|
||||||
CellTarget,
|
CellTarget,
|
||||||
ElementType,
|
ElementType,
|
||||||
@@ -591,15 +587,8 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Генерируем layout для нового элемента
|
// Генерируем layout для нового элемента
|
||||||
const existingLayouts = elements.map(e => e.layout).filter(Boolean)
|
|
||||||
const defaultLayout = generateDefaultLayout(elements.length)
|
const defaultLayout = generateDefaultLayout(elements.length)
|
||||||
|
|
||||||
const layout = findFreePosition(
|
|
||||||
defaultLayout,
|
|
||||||
existingLayouts as any[],
|
|
||||||
layoutSettings.gridSize
|
|
||||||
)
|
|
||||||
|
|
||||||
// Используем targetCells из формы или генерируем через mapToCells
|
// Используем targetCells из формы или генерируем через mapToCells
|
||||||
let targetCells = formData.targetCells || []
|
let targetCells = formData.targetCells || []
|
||||||
if (!targetCells.length) {
|
if (!targetCells.length) {
|
||||||
@@ -609,14 +598,14 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
const newElement: TemplateElement = {
|
const newElement: TemplateElement = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
type: formData.type as ElementType,
|
type: formData.type as ElementType,
|
||||||
name: formData.label?.toLowerCase().replace(/\s+/g, '_') || '',
|
name: formData.label || '',
|
||||||
label: formData.label,
|
label: formData.label,
|
||||||
targetCells,
|
targetCells,
|
||||||
placeholder: formData.placeholder,
|
placeholder: formData.placeholder,
|
||||||
required: formData.required,
|
required: formData.required,
|
||||||
options: formData.options,
|
options: formData.options,
|
||||||
order: elements.length,
|
order: elements.length,
|
||||||
layout,
|
layout: defaultLayout,
|
||||||
}
|
}
|
||||||
|
|
||||||
onElementAdd(newElement)
|
onElementAdd(newElement)
|
||||||
@@ -666,21 +655,6 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
resetForm()
|
resetForm()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAutoArrange = () => {
|
|
||||||
const arrangedElements = autoArrangeElements(
|
|
||||||
elements,
|
|
||||||
layoutSettings.gridSize
|
|
||||||
)
|
|
||||||
|
|
||||||
arrangedElements.forEach((element, index) => {
|
|
||||||
onElementUpdate(element.id, {
|
|
||||||
...element,
|
|
||||||
layout: element.layout,
|
|
||||||
order: index,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteAndSelect = (elementId: string) => {
|
const handleDeleteAndSelect = (elementId: string) => {
|
||||||
onElementDelete(elementId)
|
onElementDelete(elementId)
|
||||||
setEditingElement(null)
|
setEditingElement(null)
|
||||||
@@ -697,7 +671,6 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
<ElementFormulaBar
|
<ElementFormulaBar
|
||||||
elementCount={elements.length}
|
elementCount={elements.length}
|
||||||
onAddElement={() => setIsAddDialogOpen(true)}
|
onAddElement={() => setIsAddDialogOpen(true)}
|
||||||
onAutoArrange={handleAutoArrange}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
hasUnsavedChanges={hasUnsavedChanges}
|
hasUnsavedChanges={hasUnsavedChanges}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const HeaderBar: React.FC<HeaderBarProps> = ({ template, onBack }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => navigate(`/templates/${template.id}/elements`)}
|
onClick={() => navigate(`/templates/${template.id}/elements`)}
|
||||||
className="flex items-center gap-1 hover:bg-muted"
|
className="flex items-center gap-1 hover:bg-muted"
|
||||||
@@ -39,7 +39,7 @@ export const HeaderBar: React.FC<HeaderBarProps> = ({ template, onBack }) => {
|
|||||||
<span className="text-sm">Редактор интерфейса</span>
|
<span className="text-sm">Редактор интерфейса</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => navigate(`/templates/${template.id}/protocols`)}
|
onClick={() => navigate(`/templates/${template.id}/protocols`)}
|
||||||
className="flex items-center gap-1 hover:bg-muted"
|
className="flex items-center gap-1 hover:bg-muted"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const ELEMENT_MIN_SIZE: Record<string, { width: number; height: number }> = {
|
|||||||
radio: { width: 160, height: 80 },
|
radio: { width: 160, height: 80 },
|
||||||
checkbox: { width: 140, height: 40 },
|
checkbox: { width: 140, height: 40 },
|
||||||
standards: { width: 200, height: 50 },
|
standards: { width: 200, height: 50 },
|
||||||
calibration_conditions: { width: 250, height: 60 },
|
calibration_conditions: { width: 350, height: 50 },
|
||||||
button_group: { width: 180, height: 80 },
|
button_group: { width: 180, height: 80 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,93 +65,6 @@ export function generateDefaultLayout(order: number): ElementLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Автоматическое размещение элементов без пересечений
|
|
||||||
export function autoArrangeElements(
|
|
||||||
elements: TemplateElement[],
|
|
||||||
gridSize: number = 20
|
|
||||||
): TemplateElement[] {
|
|
||||||
const arranged = [...elements]
|
|
||||||
const cols = 3 // Количество колонок
|
|
||||||
const itemWidth = 300
|
|
||||||
const itemHeight = 80
|
|
||||||
const marginX = 20
|
|
||||||
const marginY = 20
|
|
||||||
const startX = 50
|
|
||||||
const startY = 50
|
|
||||||
|
|
||||||
arranged.forEach((element, index) => {
|
|
||||||
const col = index % cols
|
|
||||||
const row = Math.floor(index / cols)
|
|
||||||
|
|
||||||
const x = startX + col * (itemWidth + marginX)
|
|
||||||
const y = startY + row * (itemHeight + marginY)
|
|
||||||
|
|
||||||
// Выравниваем по сетке
|
|
||||||
const alignedX = Math.round(x / gridSize) * gridSize
|
|
||||||
const alignedY = Math.round(y / gridSize) * gridSize
|
|
||||||
|
|
||||||
element.layout = {
|
|
||||||
...element.layout,
|
|
||||||
x: alignedX,
|
|
||||||
y: alignedY,
|
|
||||||
width: itemWidth,
|
|
||||||
height: itemHeight,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return arranged
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверка пересечения двух элементов
|
|
||||||
export function checkElementOverlap(
|
|
||||||
element1: ElementLayout,
|
|
||||||
element2: ElementLayout
|
|
||||||
): boolean {
|
|
||||||
return !(
|
|
||||||
element1.x + element1.width <= element2.x ||
|
|
||||||
element2.x + element2.width <= element1.x ||
|
|
||||||
element1.y + element1.height <= element2.y ||
|
|
||||||
element2.y + element2.height <= element1.y
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Поиск свободного места для элемента (поиск в разумных пределах)
|
|
||||||
export function findFreePosition(
|
|
||||||
newElement: ElementLayout,
|
|
||||||
existingElements: ElementLayout[],
|
|
||||||
gridSize: number = 20
|
|
||||||
): ElementLayout {
|
|
||||||
const step = gridSize
|
|
||||||
const maxWidth = 1600 // Максимальная ширина поиска
|
|
||||||
const maxHeight = 1200 // Максимальная высота поиска
|
|
||||||
const maxAttempts = 500
|
|
||||||
let attempts = 0
|
|
||||||
|
|
||||||
for (let y = 50; y <= maxHeight - newElement.height; y += step) {
|
|
||||||
for (let x = 50; x <= maxWidth - newElement.width; x += step) {
|
|
||||||
attempts++
|
|
||||||
if (attempts > maxAttempts) break
|
|
||||||
|
|
||||||
const testPosition = { ...newElement, x, y }
|
|
||||||
const hasOverlap = existingElements.some(existing =>
|
|
||||||
checkElementOverlap(testPosition, existing)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!hasOverlap) {
|
|
||||||
return testPosition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (attempts > maxAttempts) break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если не нашли свободное место, размещаем в правом нижнем углу
|
|
||||||
return {
|
|
||||||
...newElement,
|
|
||||||
x: 50,
|
|
||||||
y: existingElements.length * 100 + 50,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Валидация адреса ячейки - теперь использует централизованную утилиту
|
// Валидация адреса ячейки - теперь использует централизованную утилиту
|
||||||
export function validateCellAddress(cell: string): boolean {
|
export function validateCellAddress(cell: string): boolean {
|
||||||
return parseCellAddress(cell) !== null
|
return parseCellAddress(cell) !== null
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button } from '@/component/ui/button'
|
|||||||
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
||||||
import { useDelayedSave } from '@/hook/useDelayedSave'
|
import { useDelayedSave } from '@/hook/useDelayedSave'
|
||||||
import { Template, TemplateElement } from '@/type/template'
|
import { Template, TemplateElement } from '@/type/template'
|
||||||
import { ArrowLeft, FileText, Wrench } from 'lucide-react'
|
import { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react'
|
||||||
import { FC, useCallback, useEffect, useState } from 'react'
|
import { FC, useCallback, useEffect, useState } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
@@ -151,9 +151,9 @@ export const ElementsCreation: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col">
|
<div className="flex h-screen flex-col">
|
||||||
<div className="flex-shrink-0 border-b bg-card px-4 py-3">
|
<div className="flex-shrink-0 border-b bg-card px-4 py-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -174,13 +174,11 @@ export const ElementsCreation: FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() =>
|
onClick={() => navigate(`/templates/${selectedTemplate.id}/edit`)}
|
||||||
navigate(`/templates/${selectedTemplate.id}/elements`)
|
className="flex items-center gap-2 hover:bg-muted"
|
||||||
}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
>
|
||||||
<Wrench className="h-3.5 w-3.5" />
|
<Settings className="h-3.5 w-3.5" />
|
||||||
Элементы интерфейса
|
Настройки шаблона
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -188,15 +186,14 @@ export const ElementsCreation: FC = () => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(`/templates/${selectedTemplate.id}/protocols`)
|
navigate(`/templates/${selectedTemplate.id}/protocols`)
|
||||||
}
|
}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-1 hover:bg-muted"
|
||||||
>
|
>
|
||||||
<FileText className="h-3.5 w-3.5" />
|
<FileText className="h-4 w-4" />
|
||||||
Создать протокол
|
<span className="text-sm">Создать протокол</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<ElementConstructor
|
<ElementConstructor
|
||||||
template={selectedTemplate}
|
template={selectedTemplate}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { cellAddressToCoordinates } from '@/lib/cell-utils'
|
|||||||
import { useToast } from '@/lib/hooks/useToast'
|
import { useToast } from '@/lib/hooks/useToast'
|
||||||
import { getLatestFileForTemplate } from '@/service/fileApiService'
|
import { getLatestFileForTemplate } from '@/service/fileApiService'
|
||||||
import { Template, TemplateElement } from '@/type/template'
|
import { Template, TemplateElement } from '@/type/template'
|
||||||
import { ArrowLeft, FileText, Grid, Save } from 'lucide-react'
|
import { ArrowLeft, FileText, Grid, Save, Settings, Wrench } from 'lucide-react'
|
||||||
import { FC, useEffect, useMemo, useRef, useState } from 'react'
|
import { FC, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
@@ -44,25 +44,23 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="relative space-y-3">
|
||||||
{/* Показываем индикатор ячеек для всех элементов */}
|
{/* Показываем индикатор ячеек для всех элементов */}
|
||||||
{element.targetCells && element.targetCells.length > 0 && (
|
{element.targetCells && element.targetCells.length > 0 && (
|
||||||
<div className="flex justify-end">
|
<div className="group/cells absolute right-0 top-0">
|
||||||
<div className="group/cells relative">
|
<Grid className="h-3 w-3 cursor-help text-gray-500 opacity-60 transition-opacity hover:opacity-100" />
|
||||||
<Grid className="h-3 w-3 cursor-help text-gray-500 opacity-60 transition-opacity hover:opacity-100" />
|
<div className="pointer-events-none absolute right-0 top-full z-50 mt-2 whitespace-nowrap rounded bg-gray-900 p-2 text-xs text-white opacity-0 shadow-lg transition-opacity group-hover/cells:opacity-100">
|
||||||
<div className="pointer-events-none absolute right-0 top-full z-50 mt-2 whitespace-nowrap rounded bg-gray-900 p-2 text-xs text-white opacity-0 shadow-lg transition-opacity group-hover/cells:opacity-100">
|
<div className="mb-1 font-medium">Целевые ячейки:</div>
|
||||||
<div className="mb-1 font-medium">Целевые ячейки:</div>
|
{element.targetCells.map((cell, i) => (
|
||||||
{element.targetCells.map((cell, i) => (
|
<div key={i} className="font-mono">
|
||||||
<div key={i} className="font-mono">
|
{cell.sheet}!{cell.cell}
|
||||||
{cell.sheet}!{cell.cell}
|
{cell.displayName && (
|
||||||
{cell.displayName && (
|
<span className="ml-1 text-gray-300">
|
||||||
<span className="ml-1 text-gray-300">
|
({cell.displayName})
|
||||||
({cell.displayName})
|
</span>
|
||||||
</span>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -78,6 +76,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
|||||||
const [showSpreadsheet, setShowSpreadsheet] = useState(false)
|
const [showSpreadsheet, setShowSpreadsheet] = useState(false)
|
||||||
const engineRef = useRef<any>(null)
|
const engineRef = useRef<any>(null)
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleFieldChange = (elementId: string, value: any) => {
|
const handleFieldChange = (elementId: string, value: any) => {
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
@@ -266,7 +265,8 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col bg-gray-50">
|
<div className="flex h-screen flex-col bg-gray-50">
|
||||||
<div className="flex-shrink-0 border-b bg-card px-4 py-2">
|
<div className="flex-shrink-0 border-b bg-card px-4 py-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="relative flex items-center">
|
||||||
|
{/* Левая часть: кнопка назад и заголовок */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="ghost" size="icon" onClick={onBack}>
|
<Button variant="ghost" size="icon" onClick={onBack}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
@@ -280,15 +280,40 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
{/* Кнопки справа */}
|
||||||
|
<div className="absolute right-0 flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => navigate(`/templates/${template.id}/edit`)}
|
||||||
|
className="flex items-center gap-1 hover:bg-muted"
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Редактор шаблона</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => navigate(`/templates/${template.id}/elements`)}
|
||||||
|
className="flex items-center gap-1 hover:bg-muted"
|
||||||
|
>
|
||||||
|
<Wrench className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Редактор интерфейса</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/* Центрированные кнопки */}
|
||||||
|
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant={showSpreadsheet ? 'default' : 'outline'}
|
variant={showSpreadsheet ? 'default' : 'outline'}
|
||||||
|
size="icon"
|
||||||
onClick={() => setShowSpreadsheet(!showSpreadsheet)}
|
onClick={() => setShowSpreadsheet(!showSpreadsheet)}
|
||||||
|
aria-label={
|
||||||
|
showSpreadsheet ? 'Скрыть таблицы' : 'Показать таблицы'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Grid className="mr-2 h-4 w-4" />
|
<Grid className="h-4 w-4" />
|
||||||
{showSpreadsheet ? 'Скрыть таблицы' : 'Показать таблицы'}
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSave} disabled={!canSave}>
|
<Button variant="outline" onClick={handleSave} disabled={!canSave}>
|
||||||
<Save className="mr-2 h-4 w-4" />
|
<Save className="mr-2 h-4 w-4" />
|
||||||
Сохранить протокол
|
Сохранить протокол
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ const TemplateCard: React.FC<TemplateCardProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
// const isConfigured =
|
|
||||||
// template.elements.length > 0 && template.excelFile !== undefined
|
|
||||||
const isConfigured = true // TODO: remove
|
const isConfigured = true // TODO: remove
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,12 +35,11 @@ const TemplateCard: React.FC<TemplateCardProps> = ({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'group relative shadow-sm transition-all duration-200',
|
|
||||||
selectionMode && 'hover:cursor-pointer',
|
selectionMode && 'hover:cursor-pointer',
|
||||||
selectionMode &&
|
selectionMode &&
|
||||||
isSelected &&
|
isSelected &&
|
||||||
'border-2 border-primary ring-4 ring-primary/30 ring-offset-2',
|
'border-2 border-primary ring-4 ring-primary/30 ring-offset-2',
|
||||||
'hover:border-2 hover:border-primary hover:shadow-lg'
|
'hover:border-2 hover:border-primary'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{selectionMode && (
|
{selectionMode && (
|
||||||
@@ -98,6 +95,7 @@ const TemplateCard: React.FC<TemplateCardProps> = ({
|
|||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
className="h-9 flex-1"
|
className="h-9 flex-1"
|
||||||
disabled={selectionMode || !isConfigured}
|
disabled={selectionMode || !isConfigured}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
|
|||||||
Reference in New Issue
Block a user