import DualSpreadsheet from '@/component/DualSpreadsheet/DualSpreadsheet' import { Button } from '@/component/ui/button' import { getElementDefinition } from '@/entitiy/element/model/interface' import { useTemplateContext } from '@/entitiy/template/model/TemplateContext' import { cellAddressToCoordinates } from '@/lib/cell-utils' import { useToast } from '@/lib/hooks/useToast' import { getLatestFileForTemplate } from '@/service/fileApiService' import { Template, TemplateElement } from '@/type/template' import { ArrowLeft, FileText, Grid, Save, Settings, Wrench } from 'lucide-react' import { FC, useEffect, useMemo, useRef, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' interface ProtocolFormProps { template: Template onSave: (data: Record) => void onBack: () => void } interface FormElementProps { element: TemplateElement value: any onChange: (value: any) => void } const FormElement: FC = ({ element, value, onChange }) => { // Получаем определение элемента из реестра const elementDefinition = getElementDefinition(element.type) // Если определение элемента не найдено, показываем ошибку if (!elementDefinition) { return (
Элемент типа "{element.type}" не найден в реестре
) } const renderElement = () => ( ) return (
{/* Показываем индикатор ячеек для всех элементов */} {element.targetCells && element.targetCells.length > 0 && (
Целевые ячейки:
{element.targetCells.map((cell, i) => (
{cell.sheet}!{cell.cell} {cell.displayName && ( ({cell.displayName}) )}
))}
)} {/* Рендер элемента через единый интерфейс */} {renderElement()}
) } const ProtocolForm: FC = ({ template, onSave, onBack }) => { const [formData, setFormData] = useState>({}) const [showSpreadsheet, setShowSpreadsheet] = useState(false) const [isInitialized, setIsInitialized] = useState(false) const engineRef = useRef(null) const toast = useToast() const navigate = useNavigate() // Инициализация значений для специальных элементов useEffect(() => { if (!isInitialized && template.elements.length > 0) { const initialData: Record = {} template.elements.forEach(element => { // Получаем определение элемента const elementDefinition = getElementDefinition(element.type) if (elementDefinition?.getInitialValue) { // Используем метод getInitialValue для получения начального значения const initialValue = elementDefinition.getInitialValue(element as any) if (initialValue !== undefined) { initialData[element.id] = initialValue } } }) if (Object.keys(initialData).length > 0) { setFormData(initialData) } setIsInitialized(true) } }, [template.elements, isInitialized]) const handleFieldChange = (elementId: string, value: any) => { setFormData(prev => ({ ...prev, [elementId]: value, })) // Прямо обновляем движок, чтобы формулы пересчитались if (engineRef.current) { const element = template.elements.find(el => el.id === elementId) if (element) { // Получаем определение элемента const elementDefinition = getElementDefinition(element.type) if (elementDefinition) { // Используем единый интерфейс для получения пар ячейка-значение const cellValues = elementDefinition.mapToCellValues( element as any, value ) // Записываем все значения в соответствующие ячейки cellValues.forEach(({ target, value: cellValue }) => { const { row, col } = cellAddressToCoordinates(target.cell) engineRef.current.setCellValueWithoutRecalc( target.sheet, row, col, cellValue ) }) engineRef.current.debouncedRecalc() } } } } const handleSave = async () => { // Ищем элемент с названием протокола (должен иметь специальный тип или название) const protocolNameElement = template.elements.find( el => el.name === 'protocolName' || el.label === 'Название протокола' ) const protocolName = protocolNameElement ? formData[protocolNameElement.id] : 'Новый протокол' const data = { protocolName, templateId: template.id, formData, createdAt: new Date(), } onSave(data) try { if (!engineRef.current) { toast.error('Движок ещё не инициализирован') return } // 1. Записываем ВСЕ значения формы в движок (поверх текущих) template.elements.forEach(el => { let value = formData[el.id] if (value === undefined || value === null) value = '' // Получаем определение элемента const elementDefinition = getElementDefinition(el.type) if (elementDefinition) { // Используем единый интерфейс для получения пар ячейка-значение const cellValues = elementDefinition.mapToCellValues(el as any, value) // Записываем все значения в соответствующие ячейки cellValues.forEach(({ target, value: cellValue }) => { const { row, col } = cellAddressToCoordinates(target.cell) if (target.sheet === 'Calculations') { toast.warning('Обнаружен лист Calculations') } const sheetName = target.sheet === 'R' || target.sheet === 'Calculations' ? 'R' : 'L' engineRef.current.setCellValueWithoutRecalc( sheetName, row, col, cellValue ) }) } }) // 2. Пересчитываем формулы engineRef.current.recalculate() // 3. Получаем изменения только левого листа относительно базового файла const allModified = engineRef.current?.getAllModifiedCells() || {} // Преобразуем в конечные (вычисленные) значения const cellsToUpdate: Record = {} const modifiedLeft = allModified['L'] || {} Object.keys(modifiedLeft).forEach(cellAddress => { const { row, col } = cellAddressToCoordinates(cellAddress) cellsToUpdate[cellAddress] = engineRef.current.getCellValue( 'L', row, col ) }) // Получаем последний файл для шаблона const latestFile = await getLatestFileForTemplate(template.id) if (!latestFile) { toast.error('Не найден исходный файл Excel для шаблона') return } const body = { cells_to_update: cellsToUpdate, file_id: latestFile.id, template_id: template.id, } const resp = await fetch('/api/protocols/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (!resp.ok) throw new Error(`HTTP ${resp.status}`) // Получаем файл как blob и скачиваем его const blob = await resp.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url // Получаем значение из ячейки R!A1 для имени файла const filenameFromCell = engineRef.current.getCellValue('R', 0, 0) // R!A1 console.log(filenameFromCell) let filename = 'Измените R!A1.xlsx' if ( filenameFromCell && typeof filenameFromCell === 'string' && filenameFromCell.trim() ) { // Очищаем имя файла от недопустимых символов const cleanName = filenameFromCell.trim().replace(/[<>:"/\\|?*]/g, '_') filename = `${cleanName}.xlsx` } // Если сервер передал имя файла в заголовке Content-Disposition, игнорируем его // и используем строго значение из R!A1 const contentDisposition = resp.headers.get('content-disposition') if (contentDisposition) { // Оставляем возможность переопределения только если R!A1 пуста if (!filenameFromCell || !filenameFromCell.toString().trim()) { const matches = contentDisposition.match( /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/ ) if (matches && matches[1]) { filename = matches[1].replace(/['"]/g, '') } } } a.download = filename document.body.appendChild(a) a.click() window.URL.revokeObjectURL(url) document.body.removeChild(a) toast.success('Протокол создан и загружен!') } catch (err) { console.error('Ошибка при создании протокола', err) toast.error('Ошибка при создании протокола') } } const canvasSize = useMemo(() => { const PADDING_Y = 200 const baseWidth = 1200 const baseHeight = 800 if (template.elements.length === 0) { return { width: baseWidth, height: baseHeight } } const contentHeight = Math.max( 0, ...template.elements.map( el => (el.layout?.y || 0) + (el.layout?.height || 0) ) ) return { width: baseWidth, height: Math.max(baseHeight, contentHeight + PADDING_Y), } }, [template.elements]) const canSave = true return (
{/* Левая часть: кнопка назад и заголовок */}

Создание протокола

{template.name}
{/* Центрированные кнопки */}
{/* Кнопки справа */}
{/* Основное содержимое */}
{ engineRef.current = engine }} enableAutoSave={false} />
{/* Форма */}
{template.elements.map(element => { const layout = element.layout || { x: 50, y: 50, width: 300, height: 80, } return (
handleFieldChange(element.id, value)} />
) })} {template.elements.length === 0 && (

Нет элементов формы

В этом шаблоне пока не созданы элементы формы

)}
) } export const ProtocolCreation: FC = () => { const navigate = useNavigate() const { templateId } = useParams<{ templateId?: string }>() const { templates } = useTemplateContext() const [selectedTemplate, setSelectedTemplate] = useState