работает

This commit is contained in:
2025-07-21 19:22:22 +03:00
parent 47e0c7f81c
commit 555505e09e
5 changed files with 146 additions and 30 deletions

View File

@@ -28,6 +28,8 @@ export const buttonGroupDefinition: ElementDefinition<
layout: 'horizontal', layout: 'horizontal',
buttonStyle: 'default', buttonStyle: 'default',
}, },
// Берём целевые ячейки прямо из конфига, если они заданы в редакторе
mapToCells: cfg => cfg.targetCells || [],
Editor: ({ config, onChange }) => { Editor: ({ config, onChange }) => {
const handleAddOption = () => { const handleAddOption = () => {
onChange({ onChange({

View File

@@ -38,7 +38,7 @@ export const ExcelUploadPanel: React.FC<ExcelUploadPanelProps> = ({
className={`h-3 w-3 ${fileName ? 'text-emerald-600' : 'text-gray-400'}`} className={`h-3 w-3 ${fileName ? 'text-emerald-600' : 'text-gray-400'}`}
/> />
<span <span
className={`max-w-32 truncate text-xs font-medium ${ className={`text-xs font-medium ${
fileName ? 'text-emerald-700' : 'text-gray-500' fileName ? 'text-emerald-700' : 'text-gray-500'
}`} }`}
> >

View File

@@ -13,9 +13,9 @@ export const HeaderBar: React.FC<HeaderBarProps> = ({ template, onBack }) => {
const navigate = useNavigate() const navigate = useNavigate()
return ( return (
<div className="flex-shrink-0 border-b bg-card px-4 py-1"> <div className="flex-shrink-0 border-b bg-card px-4 py-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
<Button variant="ghost" size="icon" onClick={onBack}> <Button variant="ghost" size="icon" onClick={onBack}>
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
</Button> </Button>
@@ -30,22 +30,22 @@ export const HeaderBar: React.FC<HeaderBarProps> = ({ template, onBack }) => {
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
variant="outline" variant="secondary"
size="sm" size="sm"
onClick={() => navigate(`/templates/${template.id}/elements`)} onClick={() => navigate(`/templates/${template.id}/elements`)}
className="flex items-center gap-2" className="flex items-center gap-1 hover:bg-muted"
> >
<Wrench className="h-3.5 w-3.5" /> <Wrench className="h-4 w-4" />
Элементы интерфейса <span className="text-sm">Элементы интерфейса</span>
</Button> </Button>
<Button <Button
variant="outline" variant="secondary"
size="sm" size="sm"
onClick={() => navigate(`/templates/${template.id}/protocols`)} onClick={() => navigate(`/templates/${template.id}/protocols`)}
className="flex items-center gap-2" className="flex items-center gap-1 hover:bg-muted"
> >
<FileText className="h-3.5 w-3.5" /> <FileText className="h-4 w-4" />
Создать протокол <span className="text-sm">Создать протокол</span>
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -79,10 +79,38 @@ export const useSheetAutoSave = (
setIsLoading(true) setIsLoading(true)
try { try {
const response = await getSheetsByTemplate(cleanTemplateId) const response = await getSheetsByTemplate(cleanTemplateId)
/*
* В API могут существовать листы как с «сырым» именем (Report/Calculations),
* так и уже нормализованные (L/R). Чтобы случайный порядок ответов не
* приводил к потере данных (один объект перезаписывал другой), собираем
* листы в map с объединением ячеек, а не простым overwrite.
*/
const sheetsMap = response.sheets.reduce( const sheetsMap = response.sheets.reduce(
(acc, sheet) => { (acc, sheet) => {
const normalizedName = sheet.name === 'Report' ? 'L' : sheet.name // Приводим названия к единым «L»/«R»
acc[normalizedName] = { ...sheet, name: normalizedName } const normalizedName =
sheet.name === 'Report'
? 'L'
: sheet.name === 'Calculations'
? 'R'
: sheet.name
if (acc[normalizedName]) {
// Уже есть лист с таким именем — мерджим данные, приоритет у более
// свежего объекта (sheet), но оставляем существующие ячейки если их
// нет в новом
acc[normalizedName] = {
...sheet,
name: normalizedName,
cells: {
...acc[normalizedName].cells,
...sheet.cells,
},
}
} else {
acc[normalizedName] = { ...sheet, name: normalizedName }
}
return acc return acc
}, },
{} as Record<string, ApiSheet> {} as Record<string, ApiSheet>

View File

@@ -15,6 +15,7 @@ import {
} from '@/component/ui/select' } from '@/component/ui/select'
import { Textarea } from '@/component/ui/textarea' import { Textarea } from '@/component/ui/textarea'
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext' import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
import { cellAddressToCoordinates } from '@/lib/cell-utils'
import { getElementDefinition } from '@/lib/element-registry' import { getElementDefinition } from '@/lib/element-registry'
import { getLatestFileForTemplate } from '@/service/fileApiService' import { getLatestFileForTemplate } from '@/service/fileApiService'
import { Template, TemplateElement } from '@/type/template' import { Template, TemplateElement } from '@/type/template'
@@ -381,7 +382,33 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
if (engineRef.current) { if (engineRef.current) {
const element = template.elements.find(el => el.id === elementId) const element = template.elements.find(el => el.id === elementId)
if (element && element.targetCells) { if (element && element.targetCells) {
element.targetCells.forEach(tc => { const cells = element.targetCells
cells.forEach((tc, idx) => {
let cellValue: any = value
// Специальная обработка для массивов эталонов
if (element.type === 'standards' && Array.isArray(value)) {
cellValue = getStandardById(value[idx] as string)?.name || ''
}
// Специальная обработка для условий калибровки
if (
element.type === 'calibration-conditions' &&
typeof value === 'object' &&
value !== null
) {
const propsOrder = [
'temperature',
'humidity',
'pressure',
'voltage',
'frequency',
'lastUpdated',
] as const
cellValue = (value as any)[propsOrder[idx]] ?? ''
}
const { row, col } = cellAddressToCoordinates(tc.cell) const { row, col } = cellAddressToCoordinates(tc.cell)
const sheetName = const sheetName =
tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L' tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
@@ -389,7 +416,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
sheetName, sheetName,
row, row,
col, col,
value cellValue
) )
}) })
engineRef.current.debouncedRecalc() engineRef.current.debouncedRecalc()
@@ -444,18 +471,78 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
if (def?.mapToCells) cells = def.mapToCells(el as any) if (def?.mapToCells) cells = def.mapToCells(el as any)
} }
cells?.forEach(tc => { // === Новая логика распределения значений по ячейкам ===
if (!cells || cells.length === 0) return
// 1) Массив (например, standards) -> каждой ячейке своё значение
if (el.type === 'standards' && Array.isArray(value)) {
cells.forEach((tc, idx) => {
const cellValue = getStandardById(value[idx] as string)?.name || ''
const { row, col } = cellAddressToCoordinates(tc.cell)
if (tc.sheet === 'Calculations') alert('Лист Calculations')
const sheetName =
tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
engineRef.current.setCellValueWithoutRecalc(
sheetName,
row,
col,
cellValue
)
})
return
}
// Обработка любых других массивов
if (Array.isArray(value)) {
cells.forEach((tc, idx) => {
const cellValue = value[idx] ?? ''
const { row, col } = cellAddressToCoordinates(tc.cell)
const sheetName =
tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
engineRef.current.setCellValueWithoutRecalc(
sheetName,
row,
col,
cellValue
)
})
return
}
// 2) Объект условия калибровки -> пишем свойства по порядку
if (
el.type === 'calibration-conditions' &&
typeof value === 'object' &&
value !== null
) {
const propsOrder = [
'temperature',
'humidity',
'pressure',
'voltage',
'frequency',
'lastUpdated',
] as const
cells.forEach((tc, idx) => {
const cellValue = (value as any)[propsOrder[idx]] ?? ''
const { row, col } = cellAddressToCoordinates(tc.cell)
const sheetName =
tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
engineRef.current.setCellValueWithoutRecalc(
sheetName,
row,
col,
cellValue
)
})
return
}
// 3) Примитив или прочие объекты -> одно значение во все ячейки
cells.forEach(tc => {
const { row, col } = cellAddressToCoordinates(tc.cell) const { row, col } = cellAddressToCoordinates(tc.cell)
const sheetName = const sheetName =
tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L' tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
console.log(
'[DBG] write to',
tc.cell,
'sheet',
sheetName,
'value',
value
)
engineRef.current.setCellValueWithoutRecalc( engineRef.current.setCellValueWithoutRecalc(
sheetName, sheetName,
row, row,
@@ -546,9 +633,10 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
const protocolNameElement = template.elements.find( const protocolNameElement = template.elements.find(
el => el.name === 'protocolName' || el.label === 'Название протокола' el => el.name === 'protocolName' || el.label === 'Название протокола'
) )
const canSave = protocolNameElement // const canSave = protocolNameElement
? !!formData[protocolNameElement.id]?.trim() // ? !!formData[protocolNameElement.id]?.trim()
: false // : false
const canSave = true
return ( return (
<div className="flex h-screen flex-col bg-gray-50"> <div className="flex h-screen flex-col bg-gray-50">
@@ -591,9 +679,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
{/* Всегда монтируем DualSpreadsheet, скрываем/показываем через hidden */} {/* Всегда монтируем DualSpreadsheet, скрываем/показываем через hidden */}
<div className={`${showSpreadsheet ? 'block' : 'hidden'} h-full`}> <div className={`${showSpreadsheet ? 'block' : 'hidden'} h-full`}>
<DualSpreadsheet <DualSpreadsheet
templateData={ templateData={template.excelData ? { L: template.excelData } : {}}
template.excelData ? { report: template.excelData } : {}
}
mergedCells={template.mergedCells || []} mergedCells={template.mergedCells || []}
templateId={template.id} templateId={template.id}
onEngineReady={engine => (engineRef.current = engine)} onEngineReady={engine => (engineRef.current = engine)}