From 71b5ea4e7b420dfb48d3ac38ab3e25abecf8b73a Mon Sep 17 00:00:00 2001 From: tlartem Date: Sun, 20 Jul 2025 15:25:51 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B8=D0=BB=D1=81=D1=8F=20?= =?UTF-8?q?=D0=BA=D0=B0=D0=BA=D0=BE=D0=B9=20=D1=82=D0=BE=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- .../DualSpreadsheet/DualSpreadsheet.tsx | 249 +++++++--- .../DualSpreadsheet/components/FormulaBar.tsx | 9 +- src/components/DualSpreadsheet/types.ts | 1 + .../TemplateManager/ElementConstructor.tsx | 143 +++--- .../TemplateManager/SpreadsheetViewer.tsx | 6 +- .../TemplateManager/TemplateEditor.tsx | 20 +- .../TemplateManager/TemplateManager.tsx | 452 +++++++++++------- src/contexts/TemplateContext.tsx | 171 +++++-- src/hooks/useFormulaAutoSave.ts | 18 +- src/hooks/useMultiSheetEngine.ts | 89 +++- src/hooks/useSheetAutoSave.ts | 370 ++++++++++++++ src/hooks/useSpreadsheetEngine.ts | 25 +- src/pages/ElementsCreation/Page.tsx | 222 +++++---- src/pages/ProtocolCreation/Page.tsx | 126 +++-- src/services/excelSevice.ts | 58 --- src/services/fileApiSevice.ts | 177 +++++++ src/services/formulaApiService.ts | 381 --------------- src/services/sheetApiService.ts | 204 ++++++++ src/services/templateApiService.ts | 198 ++++++++ vite.config.ts | 25 +- 21 files changed, 1910 insertions(+), 1037 deletions(-) create mode 100644 src/hooks/useSheetAutoSave.ts delete mode 100644 src/services/excelSevice.ts create mode 100644 src/services/fileApiSevice.ts delete mode 100644 src/services/formulaApiService.ts create mode 100644 src/services/sheetApiService.ts create mode 100644 src/services/templateApiService.ts diff --git a/package.json b/package.json index 7e82dcd..ac6c349 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,7 @@ "react-rnd": "^10.5.2", "react-window": "^1.8.11", "redux": "^4.2.1", - "tailwind-merge": "^3.3.1", - "xlsx": "^0.18.5" + "tailwind-merge": "^3.3.1" }, "devDependencies": { "@types/react": "^18.0.37", diff --git a/src/components/DualSpreadsheet/DualSpreadsheet.tsx b/src/components/DualSpreadsheet/DualSpreadsheet.tsx index 1ce1681..3896202 100644 --- a/src/components/DualSpreadsheet/DualSpreadsheet.tsx +++ b/src/components/DualSpreadsheet/DualSpreadsheet.tsx @@ -1,5 +1,6 @@ -import { useFormulaAutoSave } from '@/hooks/useFormulaAutoSave' import { useMultiSheetEngine } from '@/hooks/useMultiSheetEngine' +import { useSheetAutoSave } from '@/hooks/useSheetAutoSave' +import { getFileData, getLatestFileForTemplate } from '@/services/fileApiSevice' import { produce } from 'immer' import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -22,15 +23,14 @@ const DualSpreadsheet: FC = ({ mergedCells = [], templateId, }) => { - const { revision, updateRevision, ...multiSheetEngine } = useMultiSheetEngine( - { + const { revision, updateRevision, setTemplateData, ...multiSheetEngine } = + useMultiSheetEngine({ sheets: [ { name: SHEET_NAMES.report, rows: ROW_COUNT, cols: COL_COUNT }, { name: SHEET_NAMES.calculations, rows: ROW_COUNT, cols: COL_COUNT }, ], initialData: templateData || {}, - } - ) + }) const [activeSheet, setActiveSheet] = useState('report') const [selectedCell, setSelectedCell] = useState<{ @@ -40,6 +40,9 @@ const DualSpreadsheet: FC = ({ const [isEditing, setIsEditing] = useState(false) const [editingValue, setEditingValue] = useState('') // Локальное значение во время редактирования const [isInitialized, setIsInitialized] = useState(false) + const [baseFileData, setBaseFileData] = useState< + Record> + >({}) // Базовые данные файла const [columnWidths, setColumnWidths] = useState>( () => ({ @@ -74,29 +77,42 @@ const DualSpreadsheet: FC = ({ const lastRevision = useRef(-1) useEffect(() => { - const reportTemplate = templateData?.[SHEET_NAMES.report] + // Используем baseFileData как источник базовых значений для сравнения + const reportBaseData = baseFileData?.[SHEET_NAMES.report] - // Минимальная отладка templateData - if (!templateData || Object.keys(templateData).length === 0) { - console.log('⚠️ templateData пустой') - } - - if (reportTemplate) { + if (reportBaseData && Object.keys(reportBaseData).length > 0) { // Очищаем старые значения initialTemplateValues.current = {} - Object.entries(reportTemplate).forEach(([cellName, value]) => { + Object.entries(reportBaseData).forEach(([cellName, value]) => { initialTemplateValues.current[cellName] = String(value) }) console.log( - '✅ Загружено значений шаблона:', + '✅ Загружено базовых значений файла для сравнения:', Object.keys(initialTemplateValues.current).length ) } else { - console.log('⚠️ DualSpreadsheet: Данные шаблона отчета не найдены') + // Fallback: используем templateData, если baseFileData еще не загружен + const reportTemplate = templateData?.[SHEET_NAMES.report] + + if (reportTemplate && Object.keys(reportTemplate).length > 0) { + // Очищаем старые значения + initialTemplateValues.current = {} + + Object.entries(reportTemplate).forEach(([cellName, value]) => { + initialTemplateValues.current[cellName] = String(value) + }) + + console.log( + '✅ Загружено значений templateData для сравнения (fallback):', + Object.keys(initialTemplateValues.current).length + ) + } else { + console.log('⚠️ DualSpreadsheet: Нет базовых данных для сравнения') + } } - }, [templateData]) + }, [baseFileData, templateData]) useEffect(() => { if (selectedCell && activeSheet) { @@ -150,16 +166,16 @@ const DualSpreadsheet: FC = ({ const displayValue = allCellValues[cellKey] const cellName = getColumnLabel(col) + (row + 1) - const templateValue = initialTemplateValues.current[cellName] ?? '' + const baseFileValue = initialTemplateValues.current[cellName] ?? '' // Базовое значение из файла // Отладка только для измененных ячеек if ( sheetType === 'report' && - templateValue && - rawValue !== templateValue + baseFileValue && + rawValue !== baseFileValue ) { console.log(`🔧 Измененная ячейка ${cellName}:`, { - templateValue, + baseFileValue, rawValue, isModified: true, }) @@ -174,18 +190,18 @@ const DualSpreadsheet: FC = ({ return str } - const normalizedTemplate = normalizeValue(templateValue) + const normalizedBaseFile = normalizeValue(baseFileValue) const normalizedRaw = normalizeValue(rawValue) const isModified = sheetType === 'report' && - normalizedTemplate !== '' && - normalizedRaw !== normalizedTemplate + normalizedBaseFile !== '' && + normalizedRaw !== normalizedBaseFile // Отладка только для измененных ячеек if (sheetType === 'report' && isModified) { console.log(`🔍 Измененная ячейка ${cellName}:`, { - templateValue, + baseFileValue, rawValue, isModified, }) @@ -276,15 +292,15 @@ const DualSpreadsheet: FC = ({ selectedCell.row, selectedCell.col ) - const templateValue = initialTemplateValues.current[cellName] ?? '' + const baseFileValue = initialTemplateValues.current[cellName] ?? '' // Базовое значение из файла console.log('💾 Сохранение ячейки:', { cellName, oldValue, newValue: editingValue, - templateValue, + baseFileValue, isChanged: oldValue !== editingValue, - isModifiedFromTemplate: templateValue !== editingValue, + isModifiedFromBaseFile: baseFileValue !== editingValue, }) multiSheetEngine.setCellValueWithoutRecalc( @@ -494,12 +510,14 @@ const DualSpreadsheet: FC = ({ // Используем новый хук автосохранения const { isSaving, + isLoading, hasUnsavedChanges, lastSaveError, manualSave, clearError, - loadSavedFormulas, - } = useFormulaAutoSave( + loadSavedSheets, + loadSheets, // Добавляем асинхронную функцию загрузки + } = useSheetAutoSave( multiSheetEngine.getAllModifiedCells, multiSheetEngine.isCalculating, revision, @@ -510,6 +528,19 @@ const DualSpreadsheet: FC = ({ } ) + // Отладка templateId + useEffect(() => { + console.log('🆔 Текущий templateId в DualSpreadsheet:', { + templateId, + type: typeof templateId, + isValid: templateId + ? /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( + templateId + ) + : false, + }) + }, [templateId]) + // Используем хук для перетаскивания разделителя const handleDividerDrag = useCallback((newLeftPercentage: number) => { // Используем requestAnimationFrame для более плавного обновления @@ -527,59 +558,136 @@ const DualSpreadsheet: FC = ({ useEffect(() => { if (!templateId || isInitialized) return - // Проверяем, есть ли начальные данные - const hasInitialData = - Object.keys(templateData).length > 0 && - Object.values(templateData).some(sheet => Object.keys(sheet).length > 0) + // Асинхронная функция для загрузки и слияния данных + const initializeData = async () => { + let finalBaseData: Record> = {} - // Всегда загружаем сохраненные данные, если они есть - const savedFormulas = loadSavedFormulas() + // 1. ЗАГРУЖАЕМ БАЗОВЫЕ ДАННЫЕ ФАЙЛА + if (templateId) { + try { + console.log('🔍 Ищем файлы для templateId:', templateId) + const latestFile = await getLatestFileForTemplate(templateId) - if (savedFormulas && Object.keys(savedFormulas).length > 0) { - console.log('🔄 Загружаем сохраненные формулы в движок:', savedFormulas) + if (latestFile) { + console.log( + '📄 Найден файл:', + latestFile.name, + 'ID:', + latestFile.id + ) + const fileData = await getFileData(latestFile.id) - if (hasInitialData) { - // Если есть начальные данные, СЛИВАЕМ их с сохраненными изменениями - console.log('📋 Сливаем данные шаблона с сохраненными изменениями') - - // Создаем объединенные данные: шаблон + сохраненные изменения - const mergedData: Record> = {} - - // Копируем данные шаблона - Object.entries(templateData).forEach(([sheetName, sheetData]) => { - mergedData[sheetName] = { ...sheetData } - }) - - // Применяем сохраненные изменения поверх шаблона - Object.entries(savedFormulas).forEach(([sheetName, sheetData]) => { - if (!mergedData[sheetName]) { - mergedData[sheetName] = {} + // TODO: Пока getFileData возвращает пустые данные, используем templateData + // Когда API будет готов, здесь будут реальные данные файла + if (Object.keys(fileData.data).length > 0) { + finalBaseData = { [SHEET_NAMES.report]: fileData.data } + setBaseFileData(finalBaseData) + console.log('📄 Загружены базовые данные файла:', finalBaseData) + } else { + console.log( + '⚠️ getFileData вернул пустые данные, используем templateData' + ) + } + } else { + console.log('⚠️ Файл не найден для templateId:', templateId) } - Object.entries(sheetData).forEach(([cellAddress, value]) => { - mergedData[sheetName][cellAddress] = value - }) - }) - - console.log('📊 Загружаем объединенные данные (шаблон + изменения)') - multiSheetEngine.loadData(mergedData) - } else { - // Если нет начальных данных, используем только сохраненные - multiSheetEngine.loadData(savedFormulas) + } catch (error) { + console.error('❌ Ошибка при загрузке данных файла:', error) + } } - console.log('✅ Сохраненные формулы загружены в движок') - } else if (hasInitialData) { - console.log('📋 Используем только начальные данные шаблона') - } else { - console.log('⚠️ Нет данных для загрузки') + // 2. ИСПОЛЬЗУЕМ templateData КАК FALLBACK или основные данные + const hasTemplateData = + Object.keys(templateData).length > 0 && + Object.values(templateData).some(sheet => Object.keys(sheet).length > 0) + + if (hasTemplateData && Object.keys(finalBaseData).length === 0) { + finalBaseData = templateData + setBaseFileData(finalBaseData) + console.log( + '📄 Используем templateData как базовые данные:', + finalBaseData + ) + setTemplateData(templateData) + } + + // 3. ЗАГРУЖАЕМ ПОЛЬЗОВАТЕЛЬСКИЕ ИЗМЕНЕНИЯ ИЗ API + try { + console.log('🔍 Загружаем пользовательские изменения из API...') + const apiSheets = await loadSheets() + + // Преобразуем листы API в формат данных + const userChanges: Record> = {} + Object.entries(apiSheets).forEach(([sheetName, sheet]) => { + if (sheet.cells && Object.keys(sheet.cells).length > 0) { + userChanges[sheetName] = sheet.cells + } + }) + + console.log( + '📂 Загружены пользовательские изменения:', + Object.keys(userChanges) + ) + + // 4. ОБЪЕДИНЯЕМ БАЗОВЫЕ ДАННЫЕ С ПОЛЬЗОВАТЕЛЬСКИМИ ИЗМЕНЕНИЯМИ + if (Object.keys(userChanges).length > 0) { + console.log( + '🔄 Объединяем базовые данные с пользовательскими изменениями' + ) + + // Создаем объединенные данные: базовые данные файла + пользовательские изменения + const mergedData: Record> = {} + + // Копируем базовые данные файла как основу + Object.entries(finalBaseData).forEach(([sheetName, sheetData]) => { + mergedData[sheetName] = { ...sheetData } + }) + + // Применяем пользовательские изменения поверх базовых данных + Object.entries(userChanges).forEach(([sheetName, sheetData]) => { + if (!mergedData[sheetName]) { + mergedData[sheetName] = {} + } + Object.entries(sheetData).forEach(([cellAddress, value]) => { + mergedData[sheetName][cellAddress] = value + }) + }) + + console.log( + '📊 Загружаем объединенные данные (базовые + пользовательские)' + ) + multiSheetEngine.loadData(mergedData) + } else if (Object.keys(finalBaseData).length > 0) { + console.log( + '📋 Используем только базовые данные (нет пользовательских изменений)' + ) + multiSheetEngine.loadData(finalBaseData) + } else { + console.log('⚠️ Нет данных для загрузки') + } + } catch (error) { + console.error( + '❌ Ошибка при загрузке пользовательских изменений:', + error + ) + + // В случае ошибки используем только базовые данные + if (Object.keys(finalBaseData).length > 0) { + console.log('📋 Ошибка API, используем только базовые данные') + multiSheetEngine.loadData(finalBaseData) + } + } + + setIsInitialized(true) } - setIsInitialized(true) + initializeData() }, [ templateId, templateData, - loadSavedFormulas, + loadSheets, multiSheetEngine, + setTemplateData, isInitialized, ]) @@ -598,6 +706,7 @@ const DualSpreadsheet: FC = ({ formulaBarRef.current = el }} isSaving={isSaving} + isLoading={isLoading} hasUnsavedChanges={hasUnsavedChanges} lastSaveError={lastSaveError} onManualSave={manualSave} diff --git a/src/components/DualSpreadsheet/components/FormulaBar.tsx b/src/components/DualSpreadsheet/components/FormulaBar.tsx index 86e8488..c6e9f1c 100644 --- a/src/components/DualSpreadsheet/components/FormulaBar.tsx +++ b/src/components/DualSpreadsheet/components/FormulaBar.tsx @@ -14,6 +14,7 @@ export const FormulaBar = memo( onKeyDown, formulaBarRef, isSaving = false, + isLoading = false, hasUnsavedChanges = false, lastSaveError = null, onManualSave, @@ -68,7 +69,7 @@ export const FormulaBar = memo( }} onKeyDown={onKeyDown} /> - {(isCalculating || isSaving) && ( + {(isCalculating || isSaving || isLoading) && (
- {isSaving ? 'Сохранение...' : 'Расчет...'} + {isSaving + ? 'Сохранение...' + : isLoading + ? 'Загрузка...' + : 'Расчет...'}
)} diff --git a/src/components/DualSpreadsheet/types.ts b/src/components/DualSpreadsheet/types.ts index 434328d..bc98555 100644 --- a/src/components/DualSpreadsheet/types.ts +++ b/src/components/DualSpreadsheet/types.ts @@ -72,6 +72,7 @@ export interface FormulaBarProps { formulaBarRef: (el: HTMLInputElement | null) => void // Обновляем props для сохранения isSaving?: boolean + isLoading?: boolean hasUnsavedChanges?: boolean lastSaveError?: string | null onManualSave?: () => void diff --git a/src/components/TemplateManager/ElementConstructor.tsx b/src/components/TemplateManager/ElementConstructor.tsx index 7ba496e..f52610a 100644 --- a/src/components/TemplateManager/ElementConstructor.tsx +++ b/src/components/TemplateManager/ElementConstructor.tsx @@ -11,6 +11,7 @@ import { CellTarget, ElementType, FormLayoutSettings, + Template, TemplateElement, } from '@/types/template' import { @@ -18,10 +19,8 @@ import { Droplets, Edit3, Gauge, - LayoutGrid, Plus, Radio, - Settings, Thermometer, Trash2, Zap, @@ -64,10 +63,10 @@ const CellTargetEditor: React.FC<{ const handleUpdateTarget = ( index: number, field: keyof CellTarget, - value: string, + value: string ) => { const updatedTargets = targets.map((target, i) => - i === index ? { ...target, [field]: value } : target, + i === index ? { ...target, [field]: value } : target ) onChange(updatedTargets) } @@ -100,7 +99,7 @@ const CellTargetEditor: React.FC<{ + onChange={e => handleUpdateTarget(index, 'cell', e.target.value) } className="h-7 text-xs" @@ -353,7 +352,7 @@ const ElementForm: React.FC = ({ formData, setFormData }) => { : null const handleAddOption = () => { - setFormData((prev) => ({ + setFormData(prev => ({ ...prev, options: [...(prev.options || []), { value: '', label: '' }], })) @@ -362,18 +361,18 @@ const ElementForm: React.FC = ({ formData, setFormData }) => { const handleUpdateOption = ( index: number, field: 'value' | 'label', - value: string, + value: string ) => { - setFormData((prev) => ({ + setFormData(prev => ({ ...prev, options: prev.options?.map((option, i) => - i === index ? { ...option, [field]: value } : option, + i === index ? { ...option, [field]: value } : option ), })) } const handleRemoveOption = (index: number) => { - setFormData((prev) => ({ + setFormData(prev => ({ ...prev, options: prev.options?.filter((_, i) => i !== index), })) @@ -394,11 +393,11 @@ const ElementForm: React.FC = ({ formData, setFormData }) => { - setFormData((prev) => ({ ...prev, label: e.target.value })) + onChange={e => + setFormData(prev => ({ ...prev, label: e.target.value })) } className="h-9" /> @@ -462,8 +461,8 @@ const ElementForm: React.FC = ({ formData, setFormData }) => { - setFormData((prev) => ({ + onChange={e => + setFormData(prev => ({ ...prev, placeholder: e.target.value, })) @@ -477,8 +476,8 @@ const ElementForm: React.FC = ({ formData, setFormData }) => { - setFormData((prev) => ({ ...prev, required: !!checked })) + onCheckedChange={checked => + setFormData(prev => ({ ...prev, required: !!checked })) } />