From f162bcf1de4c7851e002f3a0c0184bac65a00c3a Mon Sep 17 00:00:00 2001 From: tlartem Date: Sun, 20 Jul 2025 18:36:02 +0300 Subject: [PATCH] =?UTF-8?q?API=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0?= =?UTF-8?q?=D0=B5=D1=82=20=D0=BC=D0=B0=D1=82=D1=8C=20=D0=B5=D0=B3=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DualSpreadsheet/DualSpreadsheet.tsx | 28 ++++++++-- .../DualSpreadsheet/components/Cell.tsx | 4 +- .../TemplateManager/TemplateEditor.tsx | 54 +++++++++++++++++-- src/hooks/useMultiSheetEngine.ts | 33 ++++++++---- src/hooks/useSheetAutoSave.ts | 27 ++++++++-- 5 files changed, 120 insertions(+), 26 deletions(-) diff --git a/src/components/DualSpreadsheet/DualSpreadsheet.tsx b/src/components/DualSpreadsheet/DualSpreadsheet.tsx index 3896202..2b9b4ee 100644 --- a/src/components/DualSpreadsheet/DualSpreadsheet.tsx +++ b/src/components/DualSpreadsheet/DualSpreadsheet.tsx @@ -194,9 +194,7 @@ const DualSpreadsheet: FC = ({ const normalizedRaw = normalizeValue(rawValue) const isModified = - sheetType === 'report' && - normalizedBaseFile !== '' && - normalizedRaw !== normalizedBaseFile + sheetType === 'report' && normalizedRaw !== normalizedBaseFile // Отладка только для измененных ячеек if (sheetType === 'report' && isModified) { @@ -582,6 +580,8 @@ const DualSpreadsheet: FC = ({ if (Object.keys(fileData.data).length > 0) { finalBaseData = { [SHEET_NAMES.report]: fileData.data } setBaseFileData(finalBaseData) + // Сохраняем baseline, чтобы в L попадали только пользовательские изменения + setTemplateData(finalBaseData) console.log('📄 Загружены базовые данные файла:', finalBaseData) } else { console.log( @@ -619,8 +619,22 @@ const DualSpreadsheet: FC = ({ // Преобразуем листы API в формат данных const userChanges: Record> = {} Object.entries(apiSheets).forEach(([sheetName, sheet]) => { - if (sheet.cells && Object.keys(sheet.cells).length > 0) { - userChanges[sheetName] = sheet.cells + // Нормализуем имя листа: "Report" -> "L" + const normalizedName = + sheetName === 'Report' + ? SHEET_NAMES.report + : sheetName === 'Calculations' + ? SHEET_NAMES.calculations + : sheetName + + // Интересуют только листы L и R + if ( + (normalizedName === SHEET_NAMES.report || + normalizedName === SHEET_NAMES.calculations) && + sheet.cells && + Object.keys(sheet.cells).length > 0 + ) { + userChanges[normalizedName] = sheet.cells } }) @@ -657,6 +671,10 @@ const DualSpreadsheet: FC = ({ '📊 Загружаем объединенные данные (базовые + пользовательские)' ) multiSheetEngine.loadData(mergedData) + + // Обновляем baseline, включая уже сохранённые изменения пользователя, + // чтобы их не сохранять повторно + setTemplateData(mergedData) } else if (Object.keys(finalBaseData).length > 0) { console.log( '📋 Используем только базовые данные (нет пользовательских изменений)' diff --git a/src/components/DualSpreadsheet/components/Cell.tsx b/src/components/DualSpreadsheet/components/Cell.tsx index c4c40f8..33a6942 100644 --- a/src/components/DualSpreadsheet/components/Cell.tsx +++ b/src/components/DualSpreadsheet/components/Cell.tsx @@ -29,7 +29,7 @@ export const Cell = memo( } const className = `border border-border ${isSelected ? 'bg-accent' : ''} ${ - isModified ? 'bg-yellow-400 !important' : '' + isModified && !mergedCellInfo?.isMerged ? 'bg-yellow-200' : '' }` // Отладка стилей для измененных ячеек @@ -77,7 +77,7 @@ export const Cell = memo( backgroundColor: isSelected ? 'hsl(var(--accent))' : isModified - ? '#fef3c7' + ? '#fef08a' /* тот же жёлтый оттенок, что и bg-yellow-200 */ : 'white', } } diff --git a/src/components/TemplateManager/TemplateEditor.tsx b/src/components/TemplateManager/TemplateEditor.tsx index e54ed0f..73bfd1e 100644 --- a/src/components/TemplateManager/TemplateEditor.tsx +++ b/src/components/TemplateManager/TemplateEditor.tsx @@ -1,6 +1,10 @@ -import { parseExcelFile } from '@/services/fileApiSevice' +import { + getFileData, + getLatestFileForTemplate, + parseExcelFile, +} from '@/services/fileApiSevice' import { Template } from '@/types/template' -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { ExcelUploadPanel } from './ExcelUploadPanel' import { FormattingToolbar } from './FormattingToolbar' import { HeaderBar } from './HeaderBar' @@ -21,6 +25,50 @@ export const TemplateEditor: React.FC = ({ ) const [isLoading, setIsLoading] = useState(false) const [dataVersion, setDataVersion] = useState(0) + const [serverFileName, setServerFileName] = useState() + + // При монтировании пытаемся загрузить последний файл для шаблона + useEffect(() => { + let isMounted = true + + const loadLatestFile = async () => { + try { + const latestFile = await getLatestFileForTemplate(template.id) + if (!latestFile) return + + // Сохраняем имя файла для отображения + if (isMounted) { + setServerFileName(latestFile.name) + } + + // Если у нас еще нет данных Excel – загружаем их + if ( + Object.keys(excelData).length === 0 && + latestFile && + latestFile.id + ) { + const fileData = await getFileData(latestFile.id) + + if (isMounted && Object.keys(fileData.data).length > 0) { + setExcelData(fileData.data) + setEditedTemplate(prev => ({ + ...prev, + mergedCells: fileData.mergedCells, + })) + setDataVersion(v => v + 1) + } + } + } catch (error) { + console.error('Ошибка при загрузке последнего файла шаблона:', error) + } + } + + loadLatestFile() + + return () => { + isMounted = false + } + }, []) const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] @@ -60,7 +108,7 @@ export const TemplateEditor: React.FC = ({
{}} onFileChange={handleFileChange} diff --git a/src/hooks/useMultiSheetEngine.ts b/src/hooks/useMultiSheetEngine.ts index 057bb50..03037c7 100644 --- a/src/hooks/useMultiSheetEngine.ts +++ b/src/hooks/useMultiSheetEngine.ts @@ -233,35 +233,46 @@ export function useMultiSheetEngine({ const allData = workbookRef.current.toJSON() const modifiedData: Record> = {} - // Сравниваем с изначальными данными шаблона - Object.entries(allData).forEach(([sheetName, sheetData]) => { + // Список всех листов (из движка или baseline) + const sheetNamesSet = new Set([ + ...Object.keys(allData), + ...Object.keys(templateDataRef.current), + ]) + + sheetNamesSet.forEach(sheetName => { + const sheetData = allData[sheetName] || {} const templateSheet = templateDataRef.current[sheetName] || {} + + const cellAddresses = new Set([ + ...Object.keys(sheetData), + ...Object.keys(templateSheet), + ]) + const modifiedSheet: Record = {} - Object.entries(sheetData).forEach(([cellAddress, value]) => { - const templateValue = templateSheet[cellAddress] + cellAddresses.forEach(cellAddress => { + const currentVal = sheetData[cellAddress] ?? '' + const templateVal = templateSheet[cellAddress] ?? '' - // Нормализуем значения для сравнения const normalizeValue = (val: any): string => { if (val === null || val === undefined) return '' const str = String(val).trim() - // Считаем "0" и пустую строку эквивалентными if (str === '0' || str === '') return '' return str } - const normalizedCurrent = normalizeValue(value) - const normalizedTemplate = normalizeValue(templateValue) + const normalizedCurrent = normalizeValue(currentVal) + const normalizedTemplate = normalizeValue(templateVal) - // Если значение отличается от шаблона, считаем его модификацией if (normalizedCurrent !== normalizedTemplate) { - modifiedSheet[cellAddress] = value + // Если текущего значения нет (ячейка очищена), отправляем '' + modifiedSheet[cellAddress] = currentVal console.log( `🔧 Пользовательское изменение ${sheetName}:${cellAddress}:`, { template: normalizedTemplate, current: normalizedCurrent, - value, + value: currentVal, } ) } diff --git a/src/hooks/useSheetAutoSave.ts b/src/hooks/useSheetAutoSave.ts index 8ff75e7..092c154 100644 --- a/src/hooks/useSheetAutoSave.ts +++ b/src/hooks/useSheetAutoSave.ts @@ -81,7 +81,8 @@ export const useSheetAutoSave = ( const response = await getSheetsByTemplate(cleanTemplateId) const sheetsMap = response.sheets.reduce( (acc, sheet) => { - acc[sheet.name] = sheet + const normalizedName = sheet.name === 'Report' ? 'L' : sheet.name + acc[normalizedName] = { ...sheet, name: normalizedName } return acc }, {} as Record @@ -109,6 +110,7 @@ export const useSheetAutoSave = ( // Основная функция сохранения через API const performSave = useCallback( async (data: Record>) => { + // Сохраняем все листы, но пропускаем пустые const sheetNames = Object.keys(data) if (sheetNames.length === 0) { console.log('📭 Нет данных для сохранения') @@ -154,10 +156,25 @@ export const useSheetAutoSave = ( } if (existingSheet) { - // Обновляем существующий лист - console.log(`🔄 Обновляем лист "${sheetName}"`) + // Объединяем старые и новые данные, чтобы не потерять предыдущие изменения + const mergedCells = { + ...existingSheet.cells, + ...sheetCells, + } + + // Проверяем, изменилось ли что-нибудь + const isChanged = Object.keys(sheetCells).some( + key => existingSheet.cells[key] !== sheetCells[key] + ) + + if (!isChanged) { + console.log(`⏭️ Лист "${sheetName}" без новых изменений`) + return + } + + console.log(`🔄 Обновляем лист "${sheetName}" (patch)`) await patchSheet(existingSheet.id, { - cells: sheetCells, + cells: mergedCells, }) // Обновляем локальное состояние @@ -165,7 +182,7 @@ export const useSheetAutoSave = ( ...prev, [sheetName]: { ...existingSheet, - cells: sheetCells, + cells: mergedCells, updated_at: new Date().toISOString(), }, }))