AI рефакторинг !
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useMultiSheetEngine } from '@/hook/useMultiSheetEngine'
|
||||
import { useSheetAutoSave } from '@/hook/useSheetAutoSave'
|
||||
import { getFileData, getLatestFileForTemplate } from '@/service/fileApiSevice'
|
||||
import { getFileData, getLatestFileForTemplate } from '@/service/fileApiService'
|
||||
import { produce } from 'immer'
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
@@ -24,25 +24,36 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
templateId,
|
||||
onEngineReady,
|
||||
}) => {
|
||||
// Мемоизируем конфигурацию листов
|
||||
const sheetsConfig = useMemo(
|
||||
() => [
|
||||
{ name: SHEET_NAMES.report, rows: ROW_COUNT, cols: COL_COUNT },
|
||||
{ name: SHEET_NAMES.calculations, rows: ROW_COUNT, cols: COL_COUNT },
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
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 },
|
||||
],
|
||||
sheets: sheetsConfig,
|
||||
initialData: templateData || {},
|
||||
})
|
||||
|
||||
// Мемоизируем объект для передачи родительскому компоненту
|
||||
const engineAPI = useMemo(
|
||||
() => ({
|
||||
...multiSheetEngine,
|
||||
updateRevision,
|
||||
}),
|
||||
[multiSheetEngine, updateRevision]
|
||||
)
|
||||
|
||||
// Передаем методы движка родительскому компоненту
|
||||
useEffect(() => {
|
||||
if (onEngineReady) {
|
||||
onEngineReady({
|
||||
...multiSheetEngine,
|
||||
updateRevision,
|
||||
})
|
||||
onEngineReady(engineAPI)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onEngineReady])
|
||||
}, [onEngineReady, engineAPI])
|
||||
|
||||
const [activeSheet, setActiveSheet] = useState<SheetType>('report')
|
||||
const [selectedCell, setSelectedCell] = useState<{
|
||||
@@ -56,31 +67,61 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
Record<string, Record<string, any>>
|
||||
>({}) // Базовые данные файла
|
||||
|
||||
const [columnWidths, setColumnWidths] = useState<Record<SheetType, number[]>>(
|
||||
// Мемоизируем начальное состояние ширин колонок
|
||||
const initialColumnWidths = useMemo(
|
||||
() => ({
|
||||
report: Array(COL_COUNT).fill(96),
|
||||
calculations: Array(COL_COUNT).fill(96),
|
||||
})
|
||||
}),
|
||||
[]
|
||||
)
|
||||
const gridRefs = useRef<Record<SheetType, any>>({
|
||||
report: null,
|
||||
calculations: null,
|
||||
})
|
||||
|
||||
const containerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||||
report: null,
|
||||
calculations: null,
|
||||
})
|
||||
const [columnWidths, setColumnWidths] =
|
||||
useState<Record<SheetType, number[]>>(initialColumnWidths)
|
||||
|
||||
// Мемоизируем начальные refs объекты
|
||||
const initialGridRefs = useMemo(
|
||||
() => ({
|
||||
report: null,
|
||||
calculations: null,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const initialContainerRefs = useMemo(
|
||||
() => ({
|
||||
report: null,
|
||||
calculations: null,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const initialHeaderRefs = useMemo(
|
||||
() => ({
|
||||
report: null,
|
||||
calculations: null,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const initialRowHeaderRefs = useMemo(
|
||||
() => ({
|
||||
report: null,
|
||||
calculations: null,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const gridRefs = useRef<Record<SheetType, any>>(initialGridRefs)
|
||||
const containerRefs =
|
||||
useRef<Record<SheetType, HTMLDivElement | null>>(initialContainerRefs)
|
||||
const headerRefs =
|
||||
useRef<Record<SheetType, HTMLDivElement | null>>(initialHeaderRefs)
|
||||
const rowHeaderRefs =
|
||||
useRef<Record<SheetType, HTMLDivElement | null>>(initialRowHeaderRefs)
|
||||
|
||||
const activeCellInputRef = useRef<HTMLInputElement>(null)
|
||||
const formulaBarRef = useRef<HTMLInputElement | null>(null)
|
||||
const headerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||||
report: null,
|
||||
calculations: null,
|
||||
})
|
||||
const rowHeaderRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||||
report: null,
|
||||
calculations: null,
|
||||
})
|
||||
const initialTemplateValues = useRef<Record<string, string>>({})
|
||||
|
||||
const cellDataCache = useRef<Record<string, Record<string, CachedCellData>>>(
|
||||
@@ -88,45 +129,43 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
)
|
||||
const lastRevision = useRef<number>(-1)
|
||||
|
||||
useEffect(() => {
|
||||
// Используем baseFileData как источник базовых значений для сравнения
|
||||
const reportBaseData = baseFileData?.[SHEET_NAMES.report]
|
||||
// Мемоизируем обработчик обновления начальных значений
|
||||
const updateInitialValues = useCallback(() => {
|
||||
// Обновляем начальные значения только если есть данные
|
||||
const hasBaseFileData = Object.keys(baseFileData).length > 0
|
||||
const hasTemplateData = Object.keys(templateData).length > 0
|
||||
|
||||
if (reportBaseData && Object.keys(reportBaseData).length > 0) {
|
||||
// Очищаем старые значения
|
||||
initialTemplateValues.current = {}
|
||||
|
||||
Object.entries(reportBaseData).forEach(([cellName, value]) => {
|
||||
initialTemplateValues.current[cellName] = String(value)
|
||||
if (hasBaseFileData) {
|
||||
Object.entries(baseFileData).forEach(([sheetName, sheetData]) => {
|
||||
Object.entries(sheetData).forEach(([cellAddress, value]) => {
|
||||
initialTemplateValues.current[cellAddress] = String(value || '')
|
||||
})
|
||||
})
|
||||
|
||||
console.log(
|
||||
'✅ Загружено базовых значений файла для сравнения:',
|
||||
'✅ Загружено значений baseFileData для сравнения (приоритет):',
|
||||
Object.keys(initialTemplateValues.current).length
|
||||
)
|
||||
} else if (hasTemplateData) {
|
||||
Object.entries(templateData).forEach(([sheetName, sheetData]) => {
|
||||
Object.entries(sheetData).forEach(([cellAddress, value]) => {
|
||||
initialTemplateValues.current[cellAddress] = String(value || '')
|
||||
})
|
||||
})
|
||||
console.log(
|
||||
'✅ Загружено значений templateData для сравнения (fallback):',
|
||||
Object.keys(initialTemplateValues.current).length
|
||||
)
|
||||
} else {
|
||||
// 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: Нет базовых данных для сравнения')
|
||||
}
|
||||
console.log('⚠️ DualSpreadsheet: Нет базовых данных для сравнения')
|
||||
}
|
||||
}, [baseFileData, templateData])
|
||||
|
||||
useEffect(() => {
|
||||
updateInitialValues()
|
||||
}, [updateInitialValues])
|
||||
|
||||
// Мемоизируем обработчик скролла к выбранной ячейке
|
||||
const scrollToSelectedCell = useCallback(() => {
|
||||
if (selectedCell && activeSheet) {
|
||||
const grid = gridRefs.current[activeSheet]
|
||||
grid?.scrollToItem({
|
||||
@@ -137,6 +176,10 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
}
|
||||
}, [selectedCell, activeSheet])
|
||||
|
||||
useEffect(() => {
|
||||
scrollToSelectedCell()
|
||||
}, [scrollToSelectedCell])
|
||||
|
||||
const getCachedCellData = useCallback(
|
||||
(sheetType: SheetType): Record<string, CachedCellData> => {
|
||||
const cacheKey = `${sheetType}-${revision}-${JSON.stringify(columnWidths[sheetType])}`
|
||||
@@ -522,10 +565,28 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
[]
|
||||
)
|
||||
|
||||
const [spreadsheetWidths, setSpreadsheetWidths] = useState({
|
||||
L: 0.5,
|
||||
R: 0.5,
|
||||
})
|
||||
// Мемоизируем состояние ширин таблиц
|
||||
const initialSpreadsheetWidths = useMemo(
|
||||
() => ({
|
||||
L: 0.5,
|
||||
R: 0.5,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const [spreadsheetWidths, setSpreadsheetWidths] = useState(
|
||||
initialSpreadsheetWidths
|
||||
)
|
||||
|
||||
// Мемоизируем опции автосохранения
|
||||
const autoSaveOptions = useMemo(
|
||||
() => ({
|
||||
templateId,
|
||||
autoSaveDelay: 2000,
|
||||
enableAutoSave: true,
|
||||
}),
|
||||
[templateId]
|
||||
)
|
||||
|
||||
// Используем новый хук автосохранения
|
||||
const {
|
||||
@@ -541,11 +602,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
multiSheetEngine.getAllModifiedCells,
|
||||
multiSheetEngine.isCalculating,
|
||||
revision,
|
||||
{
|
||||
templateId,
|
||||
autoSaveDelay: 2000,
|
||||
enableAutoSave: true,
|
||||
}
|
||||
autoSaveOptions
|
||||
)
|
||||
|
||||
// Отладка templateId
|
||||
@@ -574,153 +631,138 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
|
||||
const separatorRef = useDraggableDivider(handleDividerDrag)
|
||||
|
||||
// Загружаем сохраненные формулы при инициализации
|
||||
useEffect(() => {
|
||||
// Мемоизируем функцию инициализации данных
|
||||
const initializeData = useCallback(async () => {
|
||||
if (!templateId || isInitialized) return
|
||||
|
||||
// Асинхронная функция для загрузки и слияния данных
|
||||
const initializeData = async () => {
|
||||
let finalBaseData: Record<string, Record<string, any>> = {}
|
||||
let finalBaseData: Record<string, Record<string, any>> = {}
|
||||
|
||||
// 1. ЗАГРУЖАЕМ БАЗОВЫЕ ДАННЫЕ ФАЙЛА
|
||||
if (templateId) {
|
||||
try {
|
||||
console.log('🔍 Ищем файлы для templateId:', templateId)
|
||||
const latestFile = await getLatestFileForTemplate(templateId)
|
||||
|
||||
if (latestFile) {
|
||||
console.log(
|
||||
'📄 Найден файл:',
|
||||
latestFile.name,
|
||||
'ID:',
|
||||
latestFile.id
|
||||
)
|
||||
const fileData = await getFileData(latestFile.id)
|
||||
|
||||
// TODO: Пока getFileData возвращает пустые данные, используем templateData
|
||||
// Когда API будет готов, здесь будут реальные данные файла
|
||||
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(
|
||||
'⚠️ getFileData вернул пустые данные, используем templateData'
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ Файл не найден для templateId:', templateId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при загрузке данных файла:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// 1. ЗАГРУЖАЕМ БАЗОВЫЕ ДАННЫЕ ФАЙЛА
|
||||
if (templateId) {
|
||||
try {
|
||||
console.log('🔍 Загружаем пользовательские изменения из API...')
|
||||
const apiSheets = await loadSheets()
|
||||
const latestFile = await getLatestFileForTemplate(templateId)
|
||||
if (latestFile) {
|
||||
console.log(
|
||||
'📁 Найден последний файл для шаблона:',
|
||||
latestFile.filename
|
||||
)
|
||||
const fileData = await getFileData(latestFile.id)
|
||||
|
||||
// Преобразуем листы API в формат данных
|
||||
const userChanges: Record<string, Record<string, any>> = {}
|
||||
Object.entries(apiSheets).forEach(([sheetName, sheet]) => {
|
||||
// Нормализуем имя листа: "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
|
||||
// TODO: Пока getFileData возвращает пустые данные, используем templateData
|
||||
// Когда API будет готов, здесь будут реальные данные файла
|
||||
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(
|
||||
'⚠️ getFileData вернул пустые данные, используем templateData'
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ Файл не найден для templateId:', templateId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при загрузке данных файла:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 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<string, Record<string, any>> = {}
|
||||
Object.entries(apiSheets).forEach(([sheetName, sheet]) => {
|
||||
// Нормализуем имя листа: "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
|
||||
}
|
||||
})
|
||||
|
||||
console.log(
|
||||
'📂 Загружены пользовательские изменения:',
|
||||
Object.keys(userChanges)
|
||||
)
|
||||
|
||||
// 4. ОБЪЕДИНЯЕМ БАЗОВЫЕ ДАННЫЕ С ПОЛЬЗОВАТЕЛЬСКИМИ ИЗМЕНЕНИЯМИ
|
||||
if (Object.keys(userChanges).length > 0) {
|
||||
console.log(
|
||||
'🔄 Объединяем базовые данные с пользовательскими изменениями'
|
||||
)
|
||||
|
||||
// Создаем объединенные данные: базовые данные файла + пользовательские изменения
|
||||
const mergedData: Record<string, Record<string, any>> = {}
|
||||
|
||||
// Копируем базовые данные файла как основу
|
||||
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(
|
||||
'📂 Загружены пользовательские изменения:',
|
||||
Object.keys(userChanges)
|
||||
'📊 Загружаем объединенные данные (базовые + пользовательские)'
|
||||
)
|
||||
|
||||
// 4. ОБЪЕДИНЯЕМ БАЗОВЫЕ ДАННЫЕ С ПОЛЬЗОВАТЕЛЬСКИМИ ИЗМЕНЕНИЯМИ
|
||||
if (Object.keys(userChanges).length > 0) {
|
||||
console.log(
|
||||
'🔄 Объединяем базовые данные с пользовательскими изменениями'
|
||||
)
|
||||
|
||||
// Создаем объединенные данные: базовые данные файла + пользовательские изменения
|
||||
const mergedData: Record<string, Record<string, any>> = {}
|
||||
|
||||
// Копируем базовые данные файла как основу
|
||||
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)
|
||||
|
||||
// Обновляем baseline, включая уже сохранённые изменения пользователя,
|
||||
// чтобы их не сохранять повторно
|
||||
} else if (Object.keys(finalBaseData).length > 0) {
|
||||
console.log(
|
||||
'📋 Используем только базовые данные (нет пользовательских изменений)'
|
||||
)
|
||||
multiSheetEngine.loadData(finalBaseData)
|
||||
} else {
|
||||
console.log('⚠️ Нет данных для загрузки')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'❌ Ошибка при загрузке пользовательских изменений:',
|
||||
error
|
||||
multiSheetEngine.loadData(mergedData)
|
||||
} else if (Object.keys(finalBaseData).length > 0) {
|
||||
console.log(
|
||||
'📋 Используем только базовые данные (нет пользовательских изменений)'
|
||||
)
|
||||
|
||||
// В случае ошибки используем только базовые данные
|
||||
if (Object.keys(finalBaseData).length > 0) {
|
||||
console.log('📋 Ошибка API, используем только базовые данные')
|
||||
multiSheetEngine.loadData(finalBaseData)
|
||||
}
|
||||
multiSheetEngine.loadData(finalBaseData)
|
||||
} else {
|
||||
console.log('⚠️ Нет данных для загрузки')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при загрузке пользовательских изменений:', error)
|
||||
|
||||
setIsInitialized(true)
|
||||
// В случае ошибки используем только базовые данные
|
||||
if (Object.keys(finalBaseData).length > 0) {
|
||||
console.log('📋 Ошибка API, используем только базовые данные')
|
||||
multiSheetEngine.loadData(finalBaseData)
|
||||
}
|
||||
}
|
||||
|
||||
initializeData()
|
||||
setIsInitialized(true)
|
||||
}, [
|
||||
templateId,
|
||||
templateData,
|
||||
@@ -730,6 +772,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
isInitialized,
|
||||
])
|
||||
|
||||
// Загружаем сохраненные формулы при инициализации
|
||||
useEffect(() => {
|
||||
initializeData()
|
||||
}, [initializeData])
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-background">
|
||||
<FormulaBar
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
import { cellAddressToCoordinates, getColumnLabel } from '@/lib/cell-utils'
|
||||
import { MergedCell } from '@/type/template'
|
||||
import { MergedCellInfo } from './types'
|
||||
|
||||
// --- Helper Functions ---
|
||||
export const getColumnLabel = (index: number) => String.fromCharCode(65 + index)
|
||||
export { getColumnLabel }
|
||||
|
||||
// Функция для преобразования адреса ячейки в координаты
|
||||
export const cellAddressToCoords = (
|
||||
address: string
|
||||
): { row: number; col: number } => {
|
||||
const match = address.match(/^([A-Z]+)(\d+)$/)
|
||||
if (!match) return { row: 0, col: 0 }
|
||||
|
||||
const col = match[1].charCodeAt(0) - 65 // Простое преобразование для одной буквы
|
||||
const row = parseInt(match[2]) - 1
|
||||
|
||||
return { row, col }
|
||||
}
|
||||
// Вспомогательная функция для совместимости с существующим кодом
|
||||
export const cellAddressToCoords = cellAddressToCoordinates
|
||||
|
||||
// Функция для проверки, является ли ячейка частью объединенной группы
|
||||
export const findMergedCellInfo = (
|
||||
@@ -24,8 +15,8 @@ export const findMergedCellInfo = (
|
||||
mergedCells: MergedCell[]
|
||||
): MergedCellInfo => {
|
||||
for (const merged of mergedCells) {
|
||||
const fromCoords = cellAddressToCoords(merged.from)
|
||||
const toCoords = cellAddressToCoords(merged.to)
|
||||
const fromCoords = cellAddressToCoordinates(merged.from)
|
||||
const toCoords = cellAddressToCoordinates(merged.to)
|
||||
|
||||
// Проверяем, попадает ли текущая ячейка в диапазон объединения
|
||||
if (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { parseCellAddress } from '@/lib/cell-utils'
|
||||
import { ElementDefinition } from '@/lib/element-registry'
|
||||
import { parseCellAddress } from '@/lib/utils'
|
||||
import { CellTarget } from '@/type/template'
|
||||
import { Weight } from 'lucide-react'
|
||||
import { StandardsEditor } from './StandardsEditor'
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { FileText, Settings, Wrench } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Template } from '../type/template'
|
||||
import { Button } from './ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card'
|
||||
|
||||
interface TemplateCardProps {
|
||||
template: Template
|
||||
isSelected?: boolean
|
||||
}
|
||||
|
||||
const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
template,
|
||||
isSelected = false,
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
// Шаблон считается готовым, когда есть хотя бы один элемент и загружена
|
||||
// Excel-таблица (что подтверждается наличием mergedCells).
|
||||
const isConfigured =
|
||||
template.elements.length > 0 && (template.mergedCells?.length ?? 0) > 0
|
||||
|
||||
return (
|
||||
<Card className={isSelected ? 'border-2 border-blue-500' : ''}>
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-base font-medium">{template.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
{template.description && (
|
||||
<p className="mb-3 line-clamp-2 text-sm text-muted-foreground">
|
||||
{template.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="mb-3 flex justify-between text-xs text-muted-foreground">
|
||||
<span>
|
||||
Создан: {new Date(template.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
<span>
|
||||
Обновлён: {new Date(template.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
disabled={!isConfigured}
|
||||
onClick={() => navigate(`/templates/${template.id}/protocols`)}
|
||||
>
|
||||
<FileText className="mr-2 h-3 w-3" />
|
||||
{isConfigured ? 'Создать протокол' : 'Требует настройки'}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => navigate(`/templates/${template.id}/edit`)}
|
||||
>
|
||||
<Settings className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => navigate(`/templates/${template.id}/elements`)}
|
||||
>
|
||||
<Wrench className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default TemplateCard
|
||||
@@ -1,5 +1,5 @@
|
||||
import DualSpreadsheet from '@/component/DualSpreadsheet/DualSpreadsheet'
|
||||
import { ExcelParseResult } from '@/service/fileApiSevice'
|
||||
import { ExcelParseResult } from '@/service/fileApiService'
|
||||
import React from 'react'
|
||||
|
||||
interface SpreadsheetViewerProps {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
getFileData,
|
||||
getLatestFileForTemplate,
|
||||
parseExcelFile,
|
||||
} from '@/service/fileApiSevice'
|
||||
} from '@/service/fileApiService'
|
||||
import { Template } from '@/type/template'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { ExcelUploadPanel } from './ExcelUploadPanel'
|
||||
@@ -28,10 +28,8 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
||||
const [dataVersion, setDataVersion] = useState(0)
|
||||
const [serverFileName, setServerFileName] = useState<string | undefined>()
|
||||
|
||||
// Получаем функцию обновления шаблона из контекста
|
||||
const { updateTemplate } = useTemplateContext()
|
||||
|
||||
// При монтировании пытаемся загрузить последний файл для шаблона
|
||||
useEffect(() => {
|
||||
let isMounted = true
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Template } from '@/type/template'
|
||||
import { Circle, CircleCheck, FileText, Plus, Trash2 } from 'lucide-react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import TemplateCard from '../TemplateCard'
|
||||
import TemplateCard from '../../widget/template/ui/TemplateCard'
|
||||
import { Button } from '../ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
|
||||
Reference in New Issue
Block a user