diff --git a/src/components/DualSpreadsheet/DualSpreadsheet.tsx b/src/components/DualSpreadsheet/DualSpreadsheet.tsx index d0e8cad..b892f8f 100644 --- a/src/components/DualSpreadsheet/DualSpreadsheet.tsx +++ b/src/components/DualSpreadsheet/DualSpreadsheet.tsx @@ -22,6 +22,7 @@ const DualSpreadsheet: FC = ({ templateData = {}, mergedCells = [], templateId, + onEngineReady, }) => { const { revision, updateRevision, setTemplateData, ...multiSheetEngine } = useMultiSheetEngine({ @@ -32,6 +33,17 @@ const DualSpreadsheet: FC = ({ initialData: templateData || {}, }) + // Передаем методы движка родительскому компоненту + useEffect(() => { + if (onEngineReady) { + onEngineReady({ + ...multiSheetEngine, + updateRevision, + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [onEngineReady]) + const [activeSheet, setActiveSheet] = useState('report') const [selectedCell, setSelectedCell] = useState<{ row: number @@ -684,7 +696,6 @@ const DualSpreadsheet: FC = ({ // Обновляем baseline, включая уже сохранённые изменения пользователя, // чтобы их не сохранять повторно - setTemplateData(mergedData) } else if (Object.keys(finalBaseData).length > 0) { console.log( '📋 Используем только базовые данные (нет пользовательских изменений)' diff --git a/src/components/DualSpreadsheet/types.ts b/src/components/DualSpreadsheet/types.ts index 249c5df..0e656fb 100644 --- a/src/components/DualSpreadsheet/types.ts +++ b/src/components/DualSpreadsheet/types.ts @@ -29,6 +29,7 @@ export interface DualSpreadsheetProps { templateData?: Record> mergedCells?: MergedCell[] // Добавляем поддержку объединенных ячеек templateId?: string // Добавляем ID шаблона для сохранения + onEngineReady?: (engine: any) => void // Коллбэк, который отдаёт родителю методы движка } export interface CellProps { diff --git a/src/pages/ProtocolCreation/Page.tsx b/src/pages/ProtocolCreation/Page.tsx index 6f4c698..776fea8 100644 --- a/src/pages/ProtocolCreation/Page.tsx +++ b/src/pages/ProtocolCreation/Page.tsx @@ -16,6 +16,7 @@ import { import { Textarea } from '@/components/ui/textarea' import { useTemplateContext } from '@/contexts/TemplateContext' import { getElementDefinition } from '@/lib/element-registry' +import { getLatestFileForTemplate } from '@/services/fileApiSevice' import { Template, TemplateElement } from '@/types/template' import { ArrowLeft, @@ -26,7 +27,7 @@ import { Settings, Wrench, } from 'lucide-react' -import { FC, useEffect, useMemo, useState } from 'react' +import { FC, useEffect, useMemo, useRef, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' // Моковые данные для эталонов (такие же как в StandardsSelector) @@ -368,15 +369,54 @@ const FormElement: FC = ({ element, value, onChange }) => { const ProtocolForm: FC = ({ template, onSave, onBack }) => { const [formData, setFormData] = useState>({}) const [showSpreadsheet, setShowSpreadsheet] = useState(false) + const engineRef = useRef(null) + + // 👉 Хелперы для конвертации адреса ячейки + const labelToColumn = (label: string) => { + let col = 0 + for (let i = 0; i < label.length; i++) { + col = col * 26 + (label.charCodeAt(i) - 64) + } + return col - 1 // zero-based + } + + const parseCellAddress = (addr: string) => { + const match = addr.match(/([A-Z]+)(\d+)/i) + if (!match) return { row: 0, col: 0 } + const [, colLabel, rowNum] = match + return { + row: parseInt(rowNum, 10) - 1, + col: labelToColumn(colLabel), + } + } 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 && element.targetCells) { + element.targetCells.forEach(tc => { + const { row, col } = parseCellAddress(tc.cell) + const sheetName = + tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L' + engineRef.current.setCellValueWithoutRecalc( + sheetName, + row, + col, + value + ) + }) + engineRef.current.debouncedRecalc() + } + } } - const handleSave = () => { + const handleSave = async () => { // Ищем элемент с названием протокола (должен иметь специальный тип или название) const protocolNameElement = template.elements.find( el => el.name === 'protocolName' || el.label === 'Название протокола' @@ -392,6 +432,106 @@ const ProtocolForm: FC = ({ template, onSave, onBack }) => { createdAt: new Date(), } onSave(data) + + console.log( + '[DBG] template elements', + template.elements.map(e => ({ + id: e.id, + label: e.label, + target: e.targetCells, + type: e.type, + })) + ) + + try { + if (!engineRef.current) { + alert('Движок ещё не инициализирован') + return + } + + // 1. Записываем ВСЕ значения формы в движок (поверх текущих) + template.elements.forEach(el => { + let value = formData[el.id] + if (value === undefined || value === null) value = '' + + let cells = + el.targetCells && el.targetCells.length > 0 + ? el.targetCells + : undefined + if ((!cells || cells.length === 0) && el.type) { + const def = getElementDefinition(el.type as any) + if (def?.mapToCells) cells = def.mapToCells(el as any) + } + + cells?.forEach(tc => { + const { row, col } = parseCellAddress(tc.cell) + const sheetName = + tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L' + console.log( + '[DBG] write to', + tc.cell, + 'sheet', + sheetName, + 'value', + value + ) + engineRef.current.setCellValueWithoutRecalc( + sheetName, + row, + col, + value + ) + }) + }) + + // 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 } = parseCellAddress(cellAddress) + cellsToUpdate[cellAddress] = engineRef.current.getCellValue( + 'L', + row, + col + ) + }) + + console.log('[DBG] Final cells_to_update', cellsToUpdate) + + // Получаем последний файл для шаблона + const latestFile = await getLatestFileForTemplate(template.id) + if (!latestFile) { + alert('Не найден исходный файл Excel для шаблона') + return + } + + const body = { + cells_to_update: cellsToUpdate, + file_id: latestFile.id, + template_id: template.id, + } + + console.log('[DBG] POST body', body) + + 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}`) + + const result = await resp.json() + console.log('✅ Протокол создан, ID:', result.protocol_id) + } catch (err) { + console.error('Ошибка при создании протокола', err) + alert('Ошибка при создании протокола') + } } const handleExport = () => { @@ -467,74 +607,75 @@ const ProtocolForm: FC = ({ template, onSave, onBack }) => { {/* Основное содержимое */}
- {showSpreadsheet ? ( - /* Режим таблиц */ -
- -
- ) : ( - /* Режим формы */ -
-
- {template.elements.map(element => { - const layout = element.layout || { - x: 50, - y: 50, - width: 300, - height: 80, - } - return ( -
- handleFieldChange(element.id, value)} - /> -
- ) - })} + {/* Всегда монтируем DualSpreadsheet, скрываем/показываем через hidden */} +
+ (engineRef.current = engine)} + /> +
- {template.elements.length === 0 && ( -
-
- -

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

-

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

- -
+ {/* Форма */} +
+
+ {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 && ( +
+
+ +

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

+

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

+ +
+
+ )}
- )} +
)