подправка интерфейса
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Template } from '@/types/template'
|
import { Template } from '@/types/template'
|
||||||
import { ArrowLeft, FileText, Wrench } from 'lucide-react'
|
import { ArrowLeft, 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'
|
||||||
|
|
||||||
@@ -19,11 +19,13 @@ export const HeaderBar: React.FC<HeaderBarProps> = ({ template, onBack }) => {
|
|||||||
<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" />
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div className="flex items-center space-x-2">
|
||||||
<h1 className="text-lg font-semibold">{template.name}</h1>
|
<Settings className="h-5 w-5" />
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
<h1 className="text-lg font-semibold">Редактор шаблонов</h1>
|
||||||
Настройте Excel шаблон и значения отчета
|
<span className="text-sm text-muted-foreground">—</span>
|
||||||
</p>
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{template.name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ const TemplateCard: React.FC<TemplateCardProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
// Шаблон считается готовым, когда есть хотя бы один элемент и загружена
|
||||||
|
// Excel-таблица (что подтверждается наличием mergedCells).
|
||||||
|
const isConfigured =
|
||||||
|
template.elements.length > 0 && (template.mergedCells?.length ?? 0) > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={isSelected ? 'border-2 border-blue-500' : ''}>
|
<Card className={isSelected ? 'border-2 border-blue-500' : ''}>
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-4">
|
||||||
@@ -41,13 +46,11 @@ const TemplateCard: React.FC<TemplateCardProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
disabled={!template.excelFile || template.elements.length === 0}
|
disabled={!isConfigured}
|
||||||
onClick={() => navigate(`/templates/${template.id}/protocols`)}
|
onClick={() => navigate(`/templates/${template.id}/protocols`)}
|
||||||
>
|
>
|
||||||
<FileText className="mr-2 h-3 w-3" />
|
<FileText className="mr-2 h-3 w-3" />
|
||||||
{template.excelFile && template.elements.length > 0
|
{isConfigured ? 'Создать протокол' : 'Требует настройки'}
|
||||||
? 'Создать протокол'
|
|
||||||
: 'Требует настройки'}
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useTemplateContext } from '@/contexts/TemplateContext'
|
||||||
import {
|
import {
|
||||||
getFileData,
|
getFileData,
|
||||||
getLatestFileForTemplate,
|
getLatestFileForTemplate,
|
||||||
@@ -27,6 +28,9 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
|||||||
const [dataVersion, setDataVersion] = useState(0)
|
const [dataVersion, setDataVersion] = useState(0)
|
||||||
const [serverFileName, setServerFileName] = useState<string | undefined>()
|
const [serverFileName, setServerFileName] = useState<string | undefined>()
|
||||||
|
|
||||||
|
// Получаем функцию обновления шаблона из контекста
|
||||||
|
const { updateTemplate } = useTemplateContext()
|
||||||
|
|
||||||
// При монтировании пытаемся загрузить последний файл для шаблона
|
// При монтировании пытаемся загрузить последний файл для шаблона
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true
|
let isMounted = true
|
||||||
@@ -90,6 +94,13 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setEditedTemplate(updatedTemplate)
|
setEditedTemplate(updatedTemplate)
|
||||||
|
|
||||||
|
// Сохраняем шаблон с новым Excel-файлом, чтобы в списке он больше не
|
||||||
|
// помечался как «требует настройки»
|
||||||
|
await updateTemplate({
|
||||||
|
...updatedTemplate,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
setDataVersion(v => v + 1)
|
setDataVersion(v => v + 1)
|
||||||
|
|
||||||
console.log('✅ Файл загружен с ID:', fileId)
|
console.log('✅ Файл загружен с ID:', fileId)
|
||||||
|
|||||||
@@ -25,12 +25,28 @@ import { Button } from '../ui/button'
|
|||||||
import { Input } from '../ui/input'
|
import { Input } from '../ui/input'
|
||||||
import { Textarea } from '../ui/textarea'
|
import { Textarea } from '../ui/textarea'
|
||||||
|
|
||||||
|
// Минимальные размеры для разных типов элементов.
|
||||||
|
// Если понадобится скорректировать размеры для конкретного элемента,
|
||||||
|
// достаточно изменить эту таблицу.
|
||||||
|
const ELEMENT_MIN_SIZE: Record<string, { width: number; height: number }> = {
|
||||||
|
text: { width: 120, height: 40 },
|
||||||
|
textarea: { width: 160, height: 80 },
|
||||||
|
number: { width: 120, height: 40 },
|
||||||
|
date: { width: 140, height: 40 },
|
||||||
|
select: { width: 140, height: 40 },
|
||||||
|
radio: { width: 160, height: 80 },
|
||||||
|
checkbox: { width: 140, height: 40 },
|
||||||
|
standards: { width: 200, height: 50 },
|
||||||
|
'calibration-conditions': { width: 250, height: 60 },
|
||||||
|
'button-group': { width: 180, height: 50 },
|
||||||
|
}
|
||||||
|
|
||||||
interface VisualLayoutEditorProps {
|
interface VisualLayoutEditorProps {
|
||||||
elements: TemplateElement[]
|
elements: TemplateElement[]
|
||||||
layoutSettings: FormLayoutSettings
|
layoutSettings: FormLayoutSettings
|
||||||
onElementUpdate: (
|
onElementUpdate: (
|
||||||
elementId: string,
|
elementId: string,
|
||||||
updates: Partial<TemplateElement>,
|
updates: Partial<TemplateElement>
|
||||||
) => void
|
) => void
|
||||||
onElementDelete: (elementId: string) => void
|
onElementDelete: (elementId: string) => void
|
||||||
onLayoutSettingsChange: (settings: FormLayoutSettings) => void
|
onLayoutSettingsChange: (settings: FormLayoutSettings) => void
|
||||||
@@ -85,7 +101,7 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
|
|||||||
onUpdate({ ...layout, x: d.x, y: d.y })
|
onUpdate({ ...layout, x: d.x, y: d.y })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[layout, onUpdate],
|
[layout, onUpdate]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleResizeStop = useCallback(
|
const handleResizeStop = useCallback(
|
||||||
@@ -94,7 +110,7 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
|
|||||||
_dir: any,
|
_dir: any,
|
||||||
ref: HTMLElement,
|
ref: HTMLElement,
|
||||||
_delta: any,
|
_delta: any,
|
||||||
pos: { x: number; y: number },
|
pos: { x: number; y: number }
|
||||||
) => {
|
) => {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
...layout,
|
...layout,
|
||||||
@@ -104,7 +120,7 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
|
|||||||
y: pos.y,
|
y: pos.y,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[layout, onUpdate],
|
[layout, onUpdate]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleDeleteClick = useCallback(
|
const handleDeleteClick = useCallback(
|
||||||
@@ -112,7 +128,7 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onDelete()
|
onDelete()
|
||||||
},
|
},
|
||||||
[onDelete],
|
[onDelete]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleEditClick = useCallback(
|
const handleEditClick = useCallback(
|
||||||
@@ -120,7 +136,7 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onEdit?.()
|
onEdit?.()
|
||||||
},
|
},
|
||||||
[onEdit],
|
[onEdit]
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -280,19 +296,17 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
|
|||||||
}}
|
}}
|
||||||
onDragStop={handleDragStop}
|
onDragStop={handleDragStop}
|
||||||
onResizeStop={handleResizeStop}
|
onResizeStop={handleResizeStop}
|
||||||
minWidth={120}
|
minWidth={ELEMENT_MIN_SIZE[element.type]?.width ?? 120}
|
||||||
minHeight={32}
|
minHeight={ELEMENT_MIN_SIZE[element.type]?.height ?? 32}
|
||||||
bounds="parent"
|
bounds="parent"
|
||||||
className={`group ${
|
className={`group ${isSelected ? 'z-20 ring-2 ring-blue-500' : 'z-10'}`}
|
||||||
isSelected ? 'z-20 ring-2 ring-blue-500 ring-offset-2' : 'z-10'
|
|
||||||
}`}
|
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`h-full cursor-move ${
|
className={`h-full cursor-move ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'rounded-md border border-blue-200/50 bg-blue-50/20 p-2'
|
? 'rounded-md bg-blue-50/10 p-1'
|
||||||
: 'rounded-md border border-transparent bg-transparent p-1 hover:bg-gray-50/10'
|
: 'rounded-md p-1 hover:bg-gray-50/10'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Label and controls (only visible when selected or hovered) */}
|
{/* Label and controls (only visible when selected or hovered) */}
|
||||||
@@ -373,7 +387,7 @@ const DraggableElement: React.FC<DraggableElementProps> = React.memo(
|
|||||||
</div>
|
</div>
|
||||||
</Rnd>
|
</Rnd>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
|
export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
|
||||||
@@ -386,7 +400,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
console.log('VisualLayoutEditor render with elements:', elements)
|
console.log('VisualLayoutEditor render with elements:', elements)
|
||||||
const [selectedElementId, setSelectedElementId] = useState<string | null>(
|
const [selectedElementId, setSelectedElementId] = useState<string | null>(
|
||||||
null,
|
null
|
||||||
)
|
)
|
||||||
|
|
||||||
const canvasSize = useMemo(() => {
|
const canvasSize = useMemo(() => {
|
||||||
@@ -401,7 +415,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
|
|||||||
|
|
||||||
const contentHeight = Math.max(
|
const contentHeight = Math.max(
|
||||||
0,
|
0,
|
||||||
...elements.map((el) => (el.layout?.y || 0) + (el.layout?.height || 0)),
|
...elements.map(el => (el.layout?.y || 0) + (el.layout?.height || 0))
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -428,7 +442,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
|
|||||||
(elementId: string, layout: ElementLayout) => {
|
(elementId: string, layout: ElementLayout) => {
|
||||||
onElementUpdate(elementId, { layout })
|
onElementUpdate(elementId, { layout })
|
||||||
},
|
},
|
||||||
[onElementUpdate],
|
[onElementUpdate]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleCanvasClick = useCallback((e: React.MouseEvent) => {
|
const handleCanvasClick = useCallback((e: React.MouseEvent) => {
|
||||||
@@ -471,13 +485,13 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
|
|||||||
onClick={handleCanvasClick}
|
onClick={handleCanvasClick}
|
||||||
>
|
>
|
||||||
{GridPattern}
|
{GridPattern}
|
||||||
{elements.map((element) => (
|
{elements.map(element => (
|
||||||
<DraggableElement
|
<DraggableElement
|
||||||
key={element.id}
|
key={element.id}
|
||||||
element={element}
|
element={element}
|
||||||
isSelected={selectedElementId === element.id}
|
isSelected={selectedElementId === element.id}
|
||||||
onSelect={() => setSelectedElementId(element.id)}
|
onSelect={() => setSelectedElementId(element.id)}
|
||||||
onUpdate={(layout) =>
|
onUpdate={layout =>
|
||||||
handleElementUpdateCallback(element.id, layout)
|
handleElementUpdateCallback(element.id, layout)
|
||||||
}
|
}
|
||||||
onDelete={() => onElementDelete(element.id)}
|
onDelete={() => onElementDelete(element.id)}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ElementConstructor } from '@/components/TemplateManager/ElementConstruc
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useTemplateContext } from '@/contexts/TemplateContext'
|
import { useTemplateContext } from '@/contexts/TemplateContext'
|
||||||
import { Template, TemplateElement } from '@/types/template'
|
import { Template, TemplateElement } from '@/types/template'
|
||||||
import { ArrowLeft, Wrench } from 'lucide-react'
|
import { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
@@ -112,87 +112,81 @@ export const ElementsCreation: FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Показываем загрузку
|
// Компактные уведомления вместо полноэкранных блокеров
|
||||||
if (isLoading) {
|
if (isLoading || error || !selectedTemplate) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen items-center justify-center">
|
<div className="p-4">
|
||||||
<div className="text-center">
|
<div
|
||||||
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-blue-600 border-t-transparent"></div>
|
className={`rounded-md border px-4 py-2 text-sm ${
|
||||||
<p className="text-gray-600">Загрузка шаблона...</p>
|
error
|
||||||
</div>
|
? 'border-destructive/50 bg-destructive/10 text-destructive'
|
||||||
</div>
|
: !selectedTemplate
|
||||||
)
|
? 'border-destructive/50 bg-destructive/10 text-destructive'
|
||||||
}
|
: 'border-muted bg-muted/20 text-muted-foreground'
|
||||||
|
}`}
|
||||||
// Показываем ошибку
|
>
|
||||||
if (error) {
|
{isLoading && 'Загрузка шаблона...'}
|
||||||
return (
|
{error && `Ошибка загрузки: ${error}`}
|
||||||
<div className="flex h-screen items-center justify-center">
|
{!selectedTemplate && 'Шаблон не найден'}
|
||||||
<div className="text-center">
|
|
||||||
<Wrench className="mx-auto mb-4 h-16 w-16 text-red-400" />
|
|
||||||
<h3 className="mb-2 text-lg font-medium text-gray-900">
|
|
||||||
Ошибка загрузки
|
|
||||||
</h3>
|
|
||||||
<p className="mb-4 text-gray-600">{error}</p>
|
|
||||||
<Button onClick={() => navigate('/templates')}>
|
|
||||||
Назад к шаблонам
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если шаблон не найден
|
|
||||||
if (!selectedTemplate) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<Wrench className="mx-auto mb-4 h-16 w-16 text-gray-400" />
|
|
||||||
<h3 className="mb-2 text-lg font-medium text-gray-900">
|
|
||||||
Шаблон не найден
|
|
||||||
</h3>
|
|
||||||
<p className="mb-4 text-gray-600">
|
|
||||||
Выбранный шаблон не существует или был удален
|
|
||||||
</p>
|
|
||||||
<Button onClick={() => navigate('/templates')}>
|
|
||||||
Назад к шаблонам
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-background">
|
<div className="flex-shrink-0 border-b bg-card px-4 py-1">
|
||||||
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex h-14 items-center px-4">
|
<div className="flex items-center gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => navigate('/templates')}
|
onClick={() => navigate('/templates')}
|
||||||
className="mr-4"
|
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Wrench className="h-5 w-5" />
|
<Settings className="h-5 w-5" />
|
||||||
<h1 className="text-lg font-semibold">Редактор элементов</h1>
|
<h1 className="text-lg font-semibold">Редактор шаблонов</h1>
|
||||||
<span className="text-sm text-muted-foreground">—</span>
|
<span className="text-sm text-muted-foreground">—</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{selectedTemplate.name}
|
{selectedTemplate.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Индикатор сохранения */}
|
<div className="flex gap-2">
|
||||||
{isSaving && (
|
<Button
|
||||||
<div className="ml-auto flex items-center space-x-2 text-sm text-muted-foreground">
|
variant="outline"
|
||||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-600 border-t-transparent"></div>
|
size="sm"
|
||||||
<span>Сохранение...</span>
|
onClick={() =>
|
||||||
</div>
|
navigate(`/templates/${selectedTemplate.id}/elements`)
|
||||||
)}
|
}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Wrench className="h-3.5 w-3.5" />
|
||||||
|
Элементы интерфейса
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(`/templates/${selectedTemplate.id}/protocols`)
|
||||||
|
}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<FileText className="h-3.5 w-3.5" />
|
||||||
|
Создать протокол
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Индикатор сохранения */}
|
||||||
|
{isSaving && (
|
||||||
|
<div className="ml-auto flex items-center space-x-2 text-sm text-muted-foreground">
|
||||||
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-600 border-t-transparent"></div>
|
||||||
|
<span>Сохранение...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ElementConstructor
|
<ElementConstructor
|
||||||
template={selectedTemplate}
|
template={selectedTemplate}
|
||||||
onElementAdd={handleElementAdd}
|
onElementAdd={handleElementAdd}
|
||||||
|
|||||||
Reference in New Issue
Block a user