бета отправка измнений
This commit is contained in:
@@ -22,6 +22,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
templateData = {},
|
templateData = {},
|
||||||
mergedCells = [],
|
mergedCells = [],
|
||||||
templateId,
|
templateId,
|
||||||
|
onEngineReady,
|
||||||
}) => {
|
}) => {
|
||||||
const { revision, updateRevision, setTemplateData, ...multiSheetEngine } =
|
const { revision, updateRevision, setTemplateData, ...multiSheetEngine } =
|
||||||
useMultiSheetEngine({
|
useMultiSheetEngine({
|
||||||
@@ -32,6 +33,17 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
initialData: templateData || {},
|
initialData: templateData || {},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Передаем методы движка родительскому компоненту
|
||||||
|
useEffect(() => {
|
||||||
|
if (onEngineReady) {
|
||||||
|
onEngineReady({
|
||||||
|
...multiSheetEngine,
|
||||||
|
updateRevision,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [onEngineReady])
|
||||||
|
|
||||||
const [activeSheet, setActiveSheet] = useState<SheetType>('report')
|
const [activeSheet, setActiveSheet] = useState<SheetType>('report')
|
||||||
const [selectedCell, setSelectedCell] = useState<{
|
const [selectedCell, setSelectedCell] = useState<{
|
||||||
row: number
|
row: number
|
||||||
@@ -684,7 +696,6 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
|
|
||||||
// Обновляем baseline, включая уже сохранённые изменения пользователя,
|
// Обновляем baseline, включая уже сохранённые изменения пользователя,
|
||||||
// чтобы их не сохранять повторно
|
// чтобы их не сохранять повторно
|
||||||
setTemplateData(mergedData)
|
|
||||||
} else if (Object.keys(finalBaseData).length > 0) {
|
} else if (Object.keys(finalBaseData).length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
'📋 Используем только базовые данные (нет пользовательских изменений)'
|
'📋 Используем только базовые данные (нет пользовательских изменений)'
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export interface DualSpreadsheetProps {
|
|||||||
templateData?: Record<string, Record<string, any>>
|
templateData?: Record<string, Record<string, any>>
|
||||||
mergedCells?: MergedCell[] // Добавляем поддержку объединенных ячеек
|
mergedCells?: MergedCell[] // Добавляем поддержку объединенных ячеек
|
||||||
templateId?: string // Добавляем ID шаблона для сохранения
|
templateId?: string // Добавляем ID шаблона для сохранения
|
||||||
|
onEngineReady?: (engine: any) => void // Коллбэк, который отдаёт родителю методы движка
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CellProps {
|
export interface CellProps {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { useTemplateContext } from '@/contexts/TemplateContext'
|
import { useTemplateContext } from '@/contexts/TemplateContext'
|
||||||
import { getElementDefinition } from '@/lib/element-registry'
|
import { getElementDefinition } from '@/lib/element-registry'
|
||||||
|
import { getLatestFileForTemplate } from '@/services/fileApiSevice'
|
||||||
import { Template, TemplateElement } from '@/types/template'
|
import { Template, TemplateElement } from '@/types/template'
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
@@ -26,7 +27,7 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
Wrench,
|
Wrench,
|
||||||
} from 'lucide-react'
|
} 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'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
// Моковые данные для эталонов (такие же как в StandardsSelector)
|
// Моковые данные для эталонов (такие же как в StandardsSelector)
|
||||||
@@ -368,15 +369,54 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
|
|||||||
const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
||||||
const [formData, setFormData] = useState<Record<string, any>>({})
|
const [formData, setFormData] = useState<Record<string, any>>({})
|
||||||
const [showSpreadsheet, setShowSpreadsheet] = useState(false)
|
const [showSpreadsheet, setShowSpreadsheet] = useState(false)
|
||||||
|
const engineRef = useRef<any>(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) => {
|
const handleFieldChange = (elementId: string, value: any) => {
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[elementId]: value,
|
[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(
|
const protocolNameElement = template.elements.find(
|
||||||
el => el.name === 'protocolName' || el.label === 'Название протокола'
|
el => el.name === 'protocolName' || el.label === 'Название протокола'
|
||||||
@@ -392,6 +432,106 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
}
|
}
|
||||||
onSave(data)
|
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<string, any> = {}
|
||||||
|
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 = () => {
|
const handleExport = () => {
|
||||||
@@ -467,74 +607,75 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
|||||||
|
|
||||||
{/* Основное содержимое */}
|
{/* Основное содержимое */}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
{showSpreadsheet ? (
|
{/* Всегда монтируем DualSpreadsheet, скрываем/показываем через hidden */}
|
||||||
/* Режим таблиц */
|
<div className={`${showSpreadsheet ? 'block' : 'hidden'} h-full`}>
|
||||||
<div className="h-full">
|
<DualSpreadsheet
|
||||||
<DualSpreadsheet
|
templateData={
|
||||||
templateData={
|
template.excelData ? { report: template.excelData } : {}
|
||||||
template.excelData ? { report: template.excelData } : {}
|
}
|
||||||
}
|
mergedCells={template.mergedCells || []}
|
||||||
mergedCells={template.mergedCells || []}
|
templateId={template.id}
|
||||||
templateId={template.id}
|
onEngineReady={engine => (engineRef.current = engine)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
/* Режим формы */
|
|
||||||
<div className="h-full overflow-auto">
|
|
||||||
<div
|
|
||||||
className="relative mx-auto my-8"
|
|
||||||
style={{ width: canvasSize.width, height: canvasSize.height }}
|
|
||||||
>
|
|
||||||
{template.elements.map(element => {
|
|
||||||
const layout = element.layout || {
|
|
||||||
x: 50,
|
|
||||||
y: 50,
|
|
||||||
width: 300,
|
|
||||||
height: 80,
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={element.id}
|
|
||||||
className="absolute"
|
|
||||||
style={{
|
|
||||||
left: layout.x,
|
|
||||||
top: layout.y,
|
|
||||||
width: layout.width,
|
|
||||||
minHeight: layout.height,
|
|
||||||
zIndex: layout.zIndex || 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormElement
|
|
||||||
element={element}
|
|
||||||
value={formData[element.id]}
|
|
||||||
onChange={value => handleFieldChange(element.id, value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{template.elements.length === 0 && (
|
{/* Форма */}
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div
|
||||||
<div className="text-center text-gray-400">
|
className={`${showSpreadsheet ? 'hidden' : 'block'} h-full overflow-auto`}
|
||||||
<FileText className="mx-auto mb-4 h-16 w-16" />
|
>
|
||||||
<h3 className="mb-2 text-lg font-medium text-gray-900">
|
<div
|
||||||
Нет элементов формы
|
className="relative mx-auto my-8"
|
||||||
</h3>
|
style={{ width: canvasSize.width, height: canvasSize.height }}
|
||||||
<p className="mb-4 text-gray-600">
|
>
|
||||||
В этом шаблоне пока не созданы элементы формы
|
{template.elements.map(element => {
|
||||||
</p>
|
const layout = element.layout || {
|
||||||
<Button
|
x: 50,
|
||||||
variant="outline"
|
y: 50,
|
||||||
onClick={() => window.history.back()}
|
width: 300,
|
||||||
>
|
height: 80,
|
||||||
Настроить шаблон
|
}
|
||||||
</Button>
|
return (
|
||||||
</div>
|
<div
|
||||||
|
key={element.id}
|
||||||
|
className="absolute"
|
||||||
|
style={{
|
||||||
|
left: layout.x,
|
||||||
|
top: layout.y,
|
||||||
|
width: layout.width,
|
||||||
|
minHeight: layout.height,
|
||||||
|
zIndex: layout.zIndex || 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormElement
|
||||||
|
element={element}
|
||||||
|
value={formData[element.id]}
|
||||||
|
onChange={value => handleFieldChange(element.id, value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
</div>
|
})}
|
||||||
|
|
||||||
|
{template.elements.length === 0 && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="text-center text-gray-400">
|
||||||
|
<FileText className="mx-auto mb-4 h-16 w-16" />
|
||||||
|
<h3 className="mb-2 text-lg font-medium text-gray-900">
|
||||||
|
Нет элементов формы
|
||||||
|
</h3>
|
||||||
|
<p className="mb-4 text-gray-600">
|
||||||
|
В этом шаблоне пока не созданы элементы формы
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => window.history.back()}
|
||||||
|
>
|
||||||
|
Настроить шаблон
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user