переименования папок

This commit is contained in:
2025-07-21 15:02:10 +03:00
parent d0c79bd334
commit 84f0c9c6aa
72 changed files with 245 additions and 249 deletions

View File

@@ -0,0 +1,970 @@
import {
getAvailableElementTypes,
getElementDefinition,
} from '@/lib/element-registry'
import {
autoArrangeElements,
findFreePosition,
generateDefaultLayout,
} from '@/lib/utils'
import {
CellTarget,
ElementType,
FormLayoutSettings,
Template,
TemplateElement,
} from '@/type/template'
import {
Calendar,
Droplets,
Edit3,
Gauge,
Plus,
Radio,
Thermometer,
Trash2,
Zap,
} from 'lucide-react'
import React, { useState } from 'react'
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 {
formData: Partial<TemplateElement>
setFormData: React.Dispatch<React.SetStateAction<Partial<TemplateElement>>>
}
const CellTargetEditor: React.FC<{
targets: CellTarget[]
onChange: (targets: CellTarget[]) => void
}> = ({ targets, onChange }) => {
const handleAddTarget = () => {
onChange([...targets, { sheet: 'L', cell: '' }])
}
const handleUpdateTarget = (
index: number,
field: keyof CellTarget,
value: string
) => {
const updatedTargets = targets.map((target, i) =>
i === index ? { ...target, [field]: value } : target
)
onChange(updatedTargets)
}
const handleRemoveTarget = (index: number) => {
onChange(targets.filter((_, i) => i !== index))
}
return (
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Целевые ячейки</label>
<Button
variant="outline"
size="sm"
onClick={handleAddTarget}
className="h-7 px-2 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
Добавить
</Button>
</div>
<div className="max-h-40 space-y-2 overflow-y-auto">
{targets.map((target, index) => (
<div key={index} className="rounded border bg-gray-50/80 p-2">
<div className="mb-1.5 grid grid-cols-2 gap-2">
<div>
<label className="text-xs font-medium text-gray-600">
Лист
</label>
<Select
value={target.sheet}
onValueChange={value =>
handleUpdateTarget(index, 'sheet', value)
}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="L">L (Левый)</SelectItem>
<SelectItem value="R">R (Правый)</SelectItem>
<SelectItem value="Report">Report</SelectItem>
<SelectItem value="Calculations">Calculations</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="text-xs font-medium text-gray-600">
Ячейка
</label>
<Input
placeholder="A1, B5..."
value={target.cell}
onChange={e =>
handleUpdateTarget(index, 'cell', e.target.value)
}
className="h-7 text-xs"
/>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveTarget(index)}
className="h-5 px-1 text-xs text-red-600 hover:bg-red-50 hover:text-red-700"
>
<Trash2 className="mr-1 h-2.5 w-2.5" />
Удалить
</Button>
</div>
))}
{targets.length === 0 && (
<p className="py-3 text-center text-xs text-gray-500">
Добавьте ячейки для связи с элементом
</p>
)}
</div>
</div>
)
}
// Live Preview компонент
const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({
element,
}) => {
const elementDefinition = element.type
? getElementDefinition(element.type)
: null
if (elementDefinition && elementDefinition.Preview) {
return <elementDefinition.Preview config={element as any} />
}
// Fallback для старых элементов
const renderPreviewInput = () => {
switch (element.type) {
case 'text':
return (
<Input
placeholder={element.placeholder || 'Введите текст'}
disabled
className="w-full"
/>
)
case 'textarea':
return (
<Textarea
placeholder={element.placeholder || 'Введите текст'}
disabled
rows={3}
className="w-full resize-none"
/>
)
case 'number':
return (
<Input
type="number"
placeholder={element.placeholder || '0'}
disabled
className="w-full"
/>
)
case 'date':
return <Input type="date" disabled className="w-full" />
case 'select':
return (
<Select disabled>
<SelectTrigger className="w-full">
<span className="text-gray-500">
{element.placeholder || 'Выберите значение'}
</span>
</SelectTrigger>
</Select>
)
case 'radio':
return (
<div className="space-y-2">
{element.options?.length ? (
element.options.map((option, index) => (
<div key={index} className="flex items-center space-x-2">
<div className="h-4 w-4 rounded-full border border-gray-300" />
<span className="text-sm">{option.label}</span>
</div>
))
) : (
<div className="flex items-center space-x-2">
<div className="h-4 w-4 rounded-full border border-gray-300" />
<span className="text-sm text-gray-500">Вариант 1</span>
</div>
)}
</div>
)
case 'checkbox':
return (
<div className="flex items-center space-x-2">
<div className="h-4 w-4 rounded border border-gray-300" />
<span className="text-sm">
{element.placeholder || element.label || 'Отметить'}
</span>
</div>
)
case 'standards':
return (
<div className="space-y-2">
<div className="flex items-center justify-between rounded-md border border-input bg-background p-3">
<span className="text-sm text-muted-foreground">
{element.placeholder || 'Выберите эталоны'}
</span>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 opacity-50" />
<span className="text-xs text-muted-foreground">Выбрать</span>
</div>
</div>
<div className="text-xs text-muted-foreground">
Выбрано эталонов: 0
</div>
</div>
)
case 'calibration-conditions':
return (
<div className="space-y-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-1">
<Thermometer className="h-3 w-3 text-muted-foreground" />
<span className="text-foreground">20°C</span>
</div>
<div className="flex items-center gap-1">
<Droplets className="h-3 w-3 text-muted-foreground" />
<span className="text-foreground">65%</span>
</div>
<div className="flex items-center gap-1">
<Gauge className="h-3 w-3 text-muted-foreground" />
<span className="text-foreground">101.3кПа</span>
</div>
<div className="flex items-center gap-1">
<Zap className="h-3 w-3 text-muted-foreground" />
<span className="text-foreground">220В</span>
</div>
<div className="flex items-center gap-1">
<Radio className="h-3 w-3 text-muted-foreground" />
<span className="text-foreground">50Гц</span>
</div>
</div>
<Button variant="ghost" size="icon" className="h-6 w-6 p-0">
<Edit3 className="h-3 w-3" />
</Button>
</div>
</div>
)
case 'button-group':
return (
<div className="space-y-2">
<div className="flex flex-wrap gap-1.5">
{element.options?.length ? (
element.options.map((option, index) => (
<button
key={index}
className="rounded-md border border-border bg-background px-3 py-1.5 text-sm font-medium text-foreground transition-all duration-200 hover:border-primary/30 hover:bg-primary/5"
>
{option.label || `Кнопка ${index + 1}`}
</button>
))
) : (
<div className="rounded-md border border-dashed bg-muted/30 px-3 py-1.5 text-sm text-muted-foreground">
{element.placeholder || 'Добавьте кнопки'}
</div>
)}
</div>
</div>
)
default:
return (
<div className="rounded border border-gray-200 bg-gray-50 p-3 text-center text-sm text-gray-500">
Выберите тип элемента
</div>
)
}
}
return (
<div className="space-y-3">
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
<div className="rounded-lg border border-gray-200 bg-white p-4">
{element.label && (
<label className="mb-2 block text-sm font-medium text-gray-700">
{element.label}
{element.required && <span className="ml-1 text-red-500">*</span>}
</label>
)}
{renderPreviewInput()}
{element.targetCells && element.targetCells.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{element.targetCells.map((target, index) => (
<span
key={index}
className="inline-block rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700"
>
{target.sheet}!{target.cell}
</span>
))}
</div>
)}
</div>
</div>
)
}
const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
const elementDefinition = formData.type
? getElementDefinition(formData.type)
: null
const handleAddOption = () => {
setFormData(prev => ({
...prev,
options: [...(prev.options || []), { value: '', label: '' }],
}))
}
const handleUpdateOption = (
index: number,
field: 'value' | 'label',
value: string
) => {
setFormData(prev => ({
...prev,
options: prev.options?.map((option, i) =>
i === index ? { ...option, [field]: value } : option
),
}))
}
const handleRemoveOption = (index: number) => {
setFormData(prev => ({
...prev,
options: prev.options?.filter((_, i) => i !== index),
}))
}
const needsOptions =
formData.type === 'select' ||
formData.type === 'radio' ||
formData.type === 'button-group'
// Если есть определение элемента в реестре, используем его редактор
if (elementDefinition && elementDefinition.Editor) {
return (
<div className="grid grid-cols-2 gap-6">
{/* Форма настроек */}
<div className="space-y-4">
<div className="space-y-1.5">
<label className="text-sm font-medium">Тип элемента</label>
<Select
value={formData.type}
onValueChange={value => {
const newType = value as ElementType
console.log('Element type changed to:', newType)
setFormData(prev => {
const updates: Partial<TemplateElement> = { type: newType }
// Если выбран тип "standards" или "calibration-conditions", устанавливаем значения по умолчанию
if (
newType === 'standards' ||
newType === 'calibration-conditions'
) {
console.log(
`${newType} type selected, getting element definition`
)
const elementDefinition = getElementDefinition(newType)
console.log('Element definition:', elementDefinition)
if (elementDefinition && elementDefinition.defaultConfig) {
const defaultConfig =
elementDefinition.defaultConfig as any
console.log('Default config:', defaultConfig)
updates.targetCells =
elementDefinition.mapToCells?.(defaultConfig) || []
console.log('Generated targetCells:', updates.targetCells)
}
}
const newFormData = { ...prev, ...updates }
console.log('New form data:', newFormData)
return newFormData
})
}}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
{getAvailableElementTypes().map(({ type, label, icon }) => (
<SelectItem key={type} value={type}>
<div className="flex items-center gap-2">
{icon}
{label}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<label className="text-sm font-medium">Подпись</label>
<Input
placeholder="Название поля"
value={formData.label}
onChange={e =>
setFormData(prev => ({ ...prev, label: e.target.value }))
}
className="h-9"
/>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium">Подсказка</label>
<Input
placeholder="Введите значение..."
value={formData.placeholder}
onChange={e =>
setFormData(prev => ({
...prev,
placeholder: e.target.value,
}))
}
className="h-9"
/>
</div>
</div>
<div className="flex items-center space-x-2 py-1">
<Checkbox
id="required"
checked={formData.required}
onCheckedChange={checked =>
setFormData(prev => ({ ...prev, required: !!checked }))
}
/>
<label htmlFor="required" className="text-sm font-medium">
Обязательное поле
</label>
</div>
<CellTargetEditor
targets={formData.targetCells || []}
onChange={targets =>
setFormData(prev => ({ ...prev, targetCells: targets }))
}
/>
{/* Специфичный редактор элемента */}
<elementDefinition.Editor
config={formData as any}
onChange={newConfig =>
setFormData(prev => ({ ...prev, ...newConfig }))
}
/>
</div>
{/* Live Preview */}
<div className="sticky top-0 flex min-h-[50vh] items-center">
<ElementPreview element={formData} />
</div>
</div>
)
}
// Fallback для старых элементов
return (
<div className="grid grid-cols-2 gap-6">
{/* Форма настроек */}
<div className="space-y-4">
<div className="space-y-1.5">
<label className="text-sm font-medium">Тип элемента</label>
<Select
value={formData.type}
onValueChange={value => {
const newType = value as ElementType
console.log('Element type changed to (fallback):', newType)
setFormData(prev => {
const updates: Partial<TemplateElement> = { type: newType }
// Если выбран тип "standards" или "calibration-conditions", устанавливаем значения по умолчанию
if (
newType === 'standards' ||
newType === 'calibration-conditions'
) {
console.log(
`${newType} type selected (fallback), getting element definition`
)
const elementDefinition = getElementDefinition(newType)
console.log(
'Element definition (fallback):',
elementDefinition
)
if (elementDefinition && elementDefinition.defaultConfig) {
const defaultConfig = elementDefinition.defaultConfig as any
console.log('Default config (fallback):', defaultConfig)
updates.targetCells =
elementDefinition.mapToCells?.(defaultConfig) || []
console.log(
'Generated targetCells (fallback):',
updates.targetCells
)
}
}
const newFormData = { ...prev, ...updates }
console.log('New form data (fallback):', newFormData)
return newFormData
})
}}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
{getAvailableElementTypes().map(({ type, label, icon }) => (
<SelectItem key={type} value={type}>
<div className="flex items-center gap-2">
{icon}
{label}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium">Подпись</label>
<Input
placeholder="Название поля"
value={formData.label}
onChange={e =>
setFormData(prev => ({ ...prev, label: e.target.value }))
}
className="h-9"
/>
</div>
<CellTargetEditor
targets={formData.targetCells || []}
onChange={targets =>
setFormData(prev => ({ ...prev, targetCells: targets }))
}
/>
{needsOptions && (
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Варианты</label>
<Button
variant="outline"
size="sm"
onClick={handleAddOption}
className="h-7 px-2 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
Добавить
</Button>
</div>
<div className="max-h-28 space-y-1.5 overflow-y-auto">
{formData.options?.map((option, index) => (
<div key={index} className="flex gap-1.5">
<Input
placeholder="Значение"
value={option.value}
onChange={e =>
handleUpdateOption(index, 'value', e.target.value)
}
className="h-7 text-xs"
/>
<Input
placeholder="Подпись"
value={option.label}
onChange={e =>
handleUpdateOption(index, 'label', e.target.value)
}
className="h-7 text-xs"
/>
<Button
variant="ghost"
size="icon"
onClick={() => handleRemoveOption(index)}
className="h-7 w-7 text-red-600 hover:bg-red-50 hover:text-red-700"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</div>
</div>
)}
</div>
{/* Live Preview */}
<div className="sticky top-0 flex min-h-[50vh] items-center">
<ElementPreview element={formData} />
</div>
</div>
)
}
interface CanvasSettingsProps {
settings: FormLayoutSettings
onChange: (settings: FormLayoutSettings) => void
}
const CanvasSettings: React.FC<CanvasSettingsProps> = ({
settings,
onChange,
}) => {
return (
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Размер сетки (px)</label>
<Select
value={settings.gridSize.toString()}
onValueChange={value =>
onChange({ ...settings, gridSize: parseInt(value) })
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10px</SelectItem>
<SelectItem value="20">20px</SelectItem>
<SelectItem value="25">25px</SelectItem>
<SelectItem value="50">50px</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="showGrid"
checked={settings.showGrid}
onCheckedChange={checked =>
onChange({ ...settings, showGrid: !!checked })
}
/>
<label htmlFor="showGrid" className="text-sm font-medium">
Показывать сетку
</label>
</div>
</div>
)
}
interface ElementConstructorProps {
template: Template
onElementAdd: (element: TemplateElement) => void
// Принимаем частичное обновление элемента, чтобы корректно обновлять только изменённые поля
onElementUpdate: (
elementId: string,
updates: Partial<TemplateElement>
) => void
onElementDelete: (elementId: string) => void
disabled?: boolean
}
export const ElementConstructor: React.FC<ElementConstructorProps> = ({
template,
onElementAdd,
onElementUpdate,
onElementDelete,
disabled = false,
}) => {
const elements = template.elements
const layoutSettings = template.layoutSettings || {
gridSize: 20,
showGrid: true,
}
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
const [editingElement, setEditingElement] = useState<TemplateElement | null>(
null
)
const [formData, setFormData] = useState<Partial<TemplateElement>>({
type: 'text',
name: '',
label: '',
targetCells: [],
placeholder: '',
required: false,
options: [],
})
const resetForm = () => {
setFormData({
type: 'text',
label: '',
targetCells: [],
placeholder: '',
required: false,
options: [],
})
}
const handleAddElement = () => {
console.log('handleAddElement called with formData:', formData)
if (!formData.label) {
console.log('Validation failed: missing label')
return
}
// Для элементов типа "standards", "calibration-conditions" и "button-group" не требуем targetCells, так как они генерируются автоматически или не нужны
if (
formData.type !== 'standards' &&
formData.type !== 'calibration-conditions' &&
formData.type !== 'button-group' &&
!formData.targetCells?.length
) {
console.log(
'Validation failed: missing targetCells for element that requires manual targetCells'
)
return
}
// Генерируем layout для нового элемента
const existingLayouts = elements.map(e => e.layout).filter(Boolean)
const defaultLayout = generateDefaultLayout(elements.length)
const layout = findFreePosition(
defaultLayout,
existingLayouts as any[],
layoutSettings.gridSize
)
// Специальная обработка для элементов с автоматической генерацией targetCells
let targetCells = formData.targetCells || []
if (
formData.type === 'standards' ||
formData.type === 'calibration-conditions'
) {
// Генерируем targetCells автоматически, если их нет
if (!targetCells.length) {
const elementDefinition = getElementDefinition(formData.type)
if (elementDefinition && elementDefinition.defaultConfig) {
const defaultConfig = elementDefinition.defaultConfig as any
targetCells = elementDefinition.mapToCells?.(defaultConfig) || []
}
}
}
// Для button-group не нужны targetCells, так как это UI элемент
if (formData.type === 'button-group') {
targetCells = []
}
const newElement: TemplateElement = {
id: Date.now().toString(),
type: formData.type as ElementType,
name: formData.label?.toLowerCase().replace(/\s+/g, '_') || '',
label: formData.label,
targetCells,
placeholder: formData.placeholder,
required: formData.required,
options: formData.options,
order: elements.length,
layout,
}
onElementAdd(newElement)
resetForm()
setIsAddDialogOpen(false)
}
const handleEditElement = (element: TemplateElement) => {
setEditingElement(element)
setFormData(element)
}
const handleUpdateElement = () => {
if (!editingElement || !formData.label) return
// Для элементов типа "standards", "calibration-conditions" и "button-group" не требуем targetCells, так как они генерируются автоматически или не нужны
if (
formData.type !== 'standards' &&
formData.type !== 'calibration-conditions' &&
formData.type !== 'button-group' &&
!formData.targetCells?.length
)
return
const updatedData = {
...formData,
name:
formData.label?.toLowerCase().replace(/\s+/g, '_') ||
formData.name ||
'',
}
onElementUpdate(editingElement.id, updatedData)
setEditingElement(null)
resetForm()
}
const handleCancelEdit = () => {
setEditingElement(null)
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) => {
onElementDelete(elementId)
setEditingElement(null)
resetForm()
}
// const reordered = (reorderedElements: TemplateElement[]) => {
// onElementsReorder(reorderedElements);
// onLayoutSettingsChange({ ...layoutSettings, showGrid: true });
// };
return (
<div className="flex h-full w-full">
{/* Main Content */}
<div className="flex flex-1 flex-col bg-gray-50">
<div className="flex items-center justify-between border-b bg-white p-2">
<div className="px-2">
<h3 className="text-sm font-medium">Холст</h3>
<p className="text-xs text-gray-500">
Перетаскивайте элементы для настройки их расположения
</p>
</div>
<div className="flex items-center gap-2">
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm" disabled={disabled}>
<Plus className="mr-2 h-4 w-4" />
Добавить элемент
</Button>
</DialogTrigger>
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
<DialogHeader>
<DialogTitle>Добавить новый элемент</DialogTitle>
<DialogDescription>
Настройте параметры нового элемента формы.
</DialogDescription>
</DialogHeader>
<ElementForm formData={formData} setFormData={setFormData} />
<div className="flex justify-end gap-2 pt-4">
<Button
onClick={() => setIsAddDialogOpen(false)}
variant="outline"
>
Отмена
</Button>
<Button
onClick={handleAddElement}
disabled={
disabled ||
!formData.label ||
(formData.type !== 'standards' &&
formData.type !== 'calibration-conditions' &&
formData.type !== 'button-group' &&
!formData.targetCells?.length)
}
>
<Plus className="mr-2 h-4 w-4" />
Добавить элемент
</Button>
</div>
</DialogContent>
</Dialog>
{editingElement && (
<Dialog
open={!!editingElement}
onOpenChange={() => handleCancelEdit()}
>
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
<DialogHeader>
<DialogTitle>Редактировать элемент</DialogTitle>
<DialogDescription>
Измените параметры элемента формы.
</DialogDescription>
</DialogHeader>
<ElementForm formData={formData} setFormData={setFormData} />
<div className="flex justify-end gap-2 pt-4">
<Button onClick={handleCancelEdit} variant="outline">
Отмена
</Button>
<Button onClick={handleUpdateElement} disabled={disabled}>
Сохранить изменения
</Button>
</div>
</DialogContent>
</Dialog>
)}
</div>
</div>
<div className="flex-1 overflow-hidden">
<VisualLayoutEditor
elements={elements}
layoutSettings={layoutSettings}
onElementUpdate={onElementUpdate}
onElementDelete={handleDeleteAndSelect}
onLayoutSettingsChange={() => {}} // Настройки макета только для чтения
onElementEdit={handleEditElement}
/>
</div>
</div>
</div>
)
}