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