Исправлены проблемы с сохранением и убраны console.logs

This commit is contained in:
2025-07-22 07:45:13 +03:00
parent 1b17638565
commit 3275cdc661
18 changed files with 439 additions and 587 deletions

View File

@@ -153,22 +153,22 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
initialTemplateValues.current[cellAddress] = String(value || '')
})
})
console.log(
'✅ Загружено значений baseFileData для сравнения (приоритет):',
Object.keys(initialTemplateValues.current).length
)
// 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
)
// console.log(
// '✅ Загружено значений templateData для сравнения (fallback):',
// Object.keys(initialTemplateValues.current).length
// )
} else {
console.log('⚠️ DualSpreadsheet: Нет базовых данных для сравнения')
// console.log('⚠️ DualSpreadsheet: Нет базовых данных для сравнения')
}
}, [baseFileData, templateData])
@@ -250,11 +250,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
baseFileValue &&
rawValue !== baseFileValue
) {
console.log(`🔧 Измененная ячейка ${cellName}:`, {
baseFileValue,
rawValue,
isModified: true,
})
// console.log(`🔧 Измененная ячейка ${cellName}:`, {
// baseFileValue,
// rawValue,
// isModified: true,
// })
}
// Нормализуем значения для сравнения
@@ -274,11 +274,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
// Отладка только для измененных ячеек
if (sheetType === 'report' && isModified) {
console.log(`🔍 Измененная ячейка ${cellName}:`, {
baseFileValue,
rawValue,
isModified,
})
// console.log(`🔍 Измененная ячейка ${cellName}:`, {
// baseFileValue,
// rawValue,
// isModified,
// })
}
cachedData[cellKey] = {
@@ -326,10 +326,10 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
setHighlightedCells(highlights)
console.log('🎨 Подсветка ячеек для формулы:', formula, {
references,
highlights,
})
// console.log('🎨 Подсветка ячеек для формулы:', formula, {
// references,
// highlights,
// })
} catch (error) {
console.error('Ошибка при обновлении подсветки ячеек:', error)
setHighlightedCells([])
@@ -411,14 +411,14 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
)
const baseFileValue = initialTemplateValues.current[cellName] ?? '' // Базовое значение из файла
console.log('💾 Сохранение ячейки:', {
cellName,
oldValue,
newValue: editingValue,
baseFileValue,
isChanged: oldValue !== editingValue,
isModifiedFromBaseFile: baseFileValue !== editingValue,
})
// console.log('💾 Сохранение ячейки:', {
//cellName,
//oldValue,
//newValue: editingValue,
//baseFileValue,
//isChanged: oldValue !== editingValue,
//isModifiedFromBaseFile: baseFileValue !== editingValue,
//})
multiSheetEngine.setCellValueWithoutRecalc(
sheetName,
@@ -529,12 +529,12 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
const handleCellClick = useCallback(
(row: number, col: number) => {
console.log('🖱️ Обычный клик по ячейке:', {
row,
col,
isEditing,
activeSheet,
})
// console.log('🖱️ Обычный клик по ячейке:', {
// row,
//col,
//isEditing,
//activeSheet,
//})
if (isEditing) {
stopEditing(true)
@@ -548,20 +548,20 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
// Новый обработчик для кликов в режиме редактирования - вставляет ссылку
const handleCellReferenceClick = useCallback(
(targetSheetType: SheetType, row: number, col: number) => {
console.log('🔗 Клик для вставки ссылки:', {
targetSheetType,
row,
col,
isEditing,
selectedCell,
editingValue,
})
// console.log('🔗 Клик для вставки ссылки:', {
//targetSheetType,
//row,
//col,
//isEditing,
//selectedCell,
//editingValue,
//})
// Проверяем, что мы в режиме редактирования
if (!isEditing || !selectedCell) {
console.log(
'⚠️ Игнорируем клик ссылки: не в режиме редактирования или нет выбранной ячейки'
)
// console.log(
// '⚠️ Игнорируем клик ссылки: не в режиме редактирования или нет выбранной ячейки'
//)
return
}
@@ -571,19 +571,19 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
// Если это другой лист, делаем квалифицированную ссылку
if (targetSheetType !== activeSheet) {
cellReference = createQualifiedCellReference(targetSheetType, row, col)
console.log('📋 Создана межлистовая ссылка:', cellReference)
// console.log('📋 Создана межлистовая ссылка:', cellReference)
} else {
// Если это тот же лист, делаем локальную ссылку
cellReference = createCellAddress(row, col)
console.log('📋 Создана локальная ссылка:', cellReference)
// console.log('📋 Создана локальная ссылка:', cellReference)
}
// Добавляем ссылку к текущему значению редактирования
const newValue = editingValue + cellReference
console.log('✏️ Новое значение формулы:', {
oldValue: editingValue,
newValue,
})
// console.log('✏️ Новое значение формулы:', {
//oldValue: editingValue,
//newValue,
//})
setEditingValue(newValue)
@@ -739,15 +739,15 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
// Отладка 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,
})
// 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])
// Используем хук для перетаскивания разделителя
@@ -774,7 +774,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
try {
const latestFile = await getLatestFileForTemplate(templateId)
if (latestFile) {
console.log('📁 Найден последний файл для шаблона:', latestFile.name)
// console.log('📁 Найден последний файл для шаблона:', latestFile.name)
const fileData = await getFileData(latestFile.id)
// TODO: Пока getFileData возвращает пустые данные, используем templateData
@@ -784,14 +784,14 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
setBaseFileData(finalBaseData)
// Сохраняем baseline, чтобы в L попадали только пользовательские изменения
setTemplateData(finalBaseData)
console.log('📄 Загружены базовые данные файла:', finalBaseData)
// console.log('📄 Загружены базовые данные файла:', finalBaseData)
} else {
console.log(
'⚠️ getFileData вернул пустые данные, используем templateData'
)
// console.log(
// '⚠️ getFileData вернул пустые данные, используем templateData'
//)
}
} else {
console.log('⚠️ Файл не найден для templateId:', templateId)
// console.log('⚠️ Файл не найден для templateId:', templateId)
}
} catch (error) {
console.error('❌ Ошибка при загрузке данных файла:', error)
@@ -806,16 +806,16 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
if (hasTemplateData && Object.keys(finalBaseData).length === 0) {
finalBaseData = templateData
setBaseFileData(finalBaseData)
console.log(
'📄 Используем templateData как базовые данные:',
finalBaseData
)
// console.log(
// '📄 Используем templateData как базовые данные:',
// finalBaseData
//)
setTemplateData(templateData)
}
// 3. ЗАГРУЖАЕМ ПОЛЬЗОВАТЕЛЬСКИЕ ИЗМЕНЕНИЯ ИЗ API
try {
console.log('🔍 Загружаем пользовательские изменения из API...')
// console.log('🔍 Загружаем пользовательские изменения из API...')
const apiSheets = await loadSheets()
// Преобразуем листы API в формат данных
@@ -840,16 +840,16 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
}
})
console.log(
'📂 Загружены пользовательские изменения:',
Object.keys(userChanges)
)
// console.log(
// '📂 Загружены пользовательские изменения:',
// Object.keys(userChanges)
//)
// 4. ОБЪЕДИНЯЕМ БАЗОВЫЕ ДАННЫЕ С ПОЛЬЗОВАТЕЛЬСКИМИ ИЗМЕНЕНИЯМИ
if (Object.keys(userChanges).length > 0) {
console.log(
'🔄 Объединяем базовые данные с пользовательскими изменениями'
)
// console.log(
// '🔄 Объединяем базовые данные с пользовательскими изменениями'
//)
// Создаем объединенные данные: базовые данные файла + пользовательские изменения
const mergedData: Record<string, Record<string, any>> = {}
@@ -869,24 +869,24 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
})
})
console.log(
'📊 Загружаем объединенные данные (базовые + пользовательские)'
)
// console.log(
// '📊 Загружаем объединенные данные (базовые + пользовательские)'
//)
multiSheetEngine.loadData(mergedData)
} else if (Object.keys(finalBaseData).length > 0) {
console.log(
'📋 Используем только базовые данные (нет пользовательских изменений)'
)
// console.log(
// '📋 Используем только базовые данные (нет пользовательских изменений)'
//)
multiSheetEngine.loadData(finalBaseData)
} else {
console.log('⚠️ Нет данных для загрузки')
// console.log('⚠️ Нет данных для загрузки')
}
} catch (error) {
console.error('❌ Ошибка при загрузке пользовательских изменений:', error)
// В случае ошибки используем только базовые данные
if (Object.keys(finalBaseData).length > 0) {
console.log('📋 Ошибка API, используем только базовые данные')
// console.log('📋 Ошибка API, используем только базовые данные')
multiSheetEngine.loadData(finalBaseData)
}
}

View File

@@ -44,13 +44,13 @@ export const Cell = memo(
// Отладка стилей для измененных ячеек
if (isModified) {
console.log(`🎨 Стили для измененной ячейки ${rowIndex},${colIndex}:`, {
className,
isModified,
isSelected,
displayValue,
highlightColor,
})
// console.log(`🎨 Стили для измененной ячейки ${rowIndex},${colIndex}:`, //{
// className,
// isModified,
// isSelected,
// displayValue,
// highlightColor,
//})
}
// Для объединенных ячеек нужно рассчитать правильные размеры
@@ -109,14 +109,14 @@ export const Cell = memo(
const handleClick = useCallback(
(e: React.MouseEvent) => {
console.log('🖱️ Cell клик:', {
rowIndex,
colIndex,
sheetType,
isInEditMode,
isSelected,
hasReferenceHandler: !!onCellReferenceClick,
})
// console.log('🖱️ Cell клик:', {
// rowIndex,
// colIndex,
// sheetType,
// isInEditMode,
// isSelected,
//hasReferenceHandler: !!onCellReferenceClick,
//})
// Если мы в режиме редактирования, вставка была обработана на mousedown.
// Останавливаем всплытие, чтобы родительский Spreadsheet не сменил активный лист.
@@ -127,17 +127,17 @@ export const Cell = memo(
}
// Обычная логика клика (выделение ячейки)
console.log('📍 Обычный клик - выбираем ячейку:', {
rowIndex,
colIndex,
sheetType,
})
// console.log('📍 Обычный клик - выбираем ячейку:', {
// rowIndex,
// colIndex,
// sheetType,
//})
// Подсказка для пользователя
if (!isInEditMode) {
console.log(
'💡 Чтобы вставить ссылку на ячейку: 1) Выберите ячейку 2) Нажмите F2 или Enter для редактирования 3) Кликните на нужную ячейку'
)
// console.log(
// '💡 Чтобы вставить ссылку на ячейку: 1) Выберите ячейку 2) Нажмите F2 или Enter для редактирования 3) Кликните на нужную ячейку'
//)
}
setActiveSheet(sheetType)
@@ -166,11 +166,11 @@ export const Cell = memo(
if (isInEditMode && onCellReferenceClick && !isSelected) {
e.preventDefault()
e.stopPropagation()
console.log('🖱️ mousedown вставка ссылки до blur:', {
rowIndex,
colIndex,
sheetType,
})
// console.log('🖱️ mousedown вставка ссылки до blur:', {
// rowIndex,
// colIndex,
// sheetType,
//})
onCellReferenceClick(sheetType, rowIndex, colIndex)
return
}

View File

@@ -55,16 +55,16 @@ export const CellRenderer = ({
// Отладка для измененных ячеек
if (cellData?.isModified) {
console.log(
`🔍 CellRenderer: Измененная ячейка ${rowIndex},${columnIndex}:`,
{
cellKey,
isModified: cellData.isModified,
displayValue,
rawValue,
highlightColor,
}
)
// console.log(
// `🔍 CellRenderer: Измененная ячейка ${rowIndex},${columnIndex}:`,
// {
// cellKey,
// isModified: cellData.isModified,
// displayValue,
// rawValue,
// highlightColor,
// }
// )
}
// Получаем информацию об объединенных ячейках только для листа отчета

View File

@@ -56,13 +56,13 @@ export const Spreadsheet = ({
}
// Отладочное логирование для проверки состояния
console.log(`📊 Spreadsheet ${sheetType} состояние:`, {
isEditing,
isInEditMode,
selectedCell,
activeSheet,
isActiveSheet: activeSheet === sheetType,
})
// console.log(`📊 Spreadsheet ${sheetType} состояние:`, {
// isEditing,
// isInEditMode,
// selectedCell,
// activeSheet,
// isActiveSheet: activeSheet === sheetType,
//})
const parentRef = useRef<HTMLDivElement>(null)
const [gridSize, setGridSize] = useState({ width: 0, height: 0 })

View File

@@ -395,7 +395,7 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
value={formData.type}
onValueChange={value => {
const newType = value as ElementType
console.log('Element type changed to:', newType)
// console.log('Element type changed to:', newType)
setFormData(prev => {
const updates: Partial<TemplateElement> = { type: newType }
@@ -405,24 +405,24 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
newType === 'standards' ||
newType === 'calibration-conditions'
) {
console.log(
`${newType} type selected, getting element definition`
)
// console.log(
// `${newType} type selected, getting element definition`
// )
const elementDefinition = getElementDefinition(newType)
console.log('Element definition:', elementDefinition)
// console.log('Element definition:', elementDefinition)
if (elementDefinition && elementDefinition.defaultConfig) {
const defaultConfig =
elementDefinition.defaultConfig as any
console.log('Default config:', defaultConfig)
// console.log('Default config:', defaultConfig)
updates.targetCells =
elementDefinition.mapToCells?.(defaultConfig) || []
console.log('Generated targetCells:', updates.targetCells)
// console.log('Generated targetCells:', updates.targetCells)
}
}
const newFormData = { ...prev, ...updates }
console.log('New form data:', newFormData)
// console.log('New form data:', newFormData)
return newFormData
})
}}
@@ -520,7 +520,7 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
value={formData.type}
onValueChange={value => {
const newType = value as ElementType
console.log('Element type changed to (fallback):', newType)
// console.log('Element type changed to (fallback):', newType)
setFormData(prev => {
const updates: Partial<TemplateElement> = { type: newType }
@@ -530,29 +530,29 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
newType === 'standards' ||
newType === 'calibration-conditions'
) {
console.log(
`${newType} type selected (fallback), getting element definition`
)
// console.log(
// `${newType} type selected (fallback), getting element definition`
// )
const elementDefinition = getElementDefinition(newType)
console.log(
'Element definition (fallback):',
elementDefinition
)
// console.log(
// 'Element definition (fallback):',
// elementDefinition
// )
if (elementDefinition && elementDefinition.defaultConfig) {
const defaultConfig = elementDefinition.defaultConfig as any
console.log('Default config (fallback):', defaultConfig)
// console.log('Default config (fallback):', defaultConfig)
updates.targetCells =
elementDefinition.mapToCells?.(defaultConfig) || []
console.log(
'Generated targetCells (fallback):',
updates.targetCells
)
// console.log(
// 'Generated targetCells (fallback):',
// updates.targetCells
// )
}
}
const newFormData = { ...prev, ...updates }
console.log('New form data (fallback):', newFormData)
// console.log('New form data (fallback):', newFormData)
return newFormData
})
}}
@@ -746,10 +746,10 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
}
const handleAddElement = () => {
console.log('handleAddElement called with formData:', formData)
// console.log('handleAddElement called with formData:', formData)
if (!formData.label) {
console.log('Validation failed: missing label')
// console.log('Validation failed: missing label')
return
}
@@ -760,9 +760,9 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
formData.type !== 'button-group' &&
!formData.targetCells?.length
) {
console.log(
'Validation failed: missing targetCells for element that requires manual targetCells'
)
// console.log(
// 'Validation failed: missing targetCells for element that requires manual targetCells'
// )
return
}

View File

@@ -101,7 +101,7 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
})
setDataVersion(v => v + 1)
console.log('✅ Файл загружен с ID:', fileId)
// console.log('✅ Файл загружен с ID:', fileId)
} catch (err) {
console.error(err)
alert('Ошибка при загрузке Excel файла на сервер')

View File

@@ -398,7 +398,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
onLayoutSettingsChange,
onElementEdit,
}) => {
console.log('VisualLayoutEditor render with elements:', elements)
// console.log('VisualLayoutEditor render with elements:', elements)
const [selectedElementId, setSelectedElementId] = useState<string | null>(
null
)

View File

@@ -15,7 +15,7 @@ export interface UseFormulaAutoSaveOptions {
// Ключ для localStorage
const getStorageKey = (templateId: string) => {
console.log('🔑 Получен templateId:', templateId)
// console.log('🔑 Получен templateId:', templateId)
return `formula_data_${templateId}`
}
@@ -58,7 +58,7 @@ export const useFormulaAutoSave = (
const parsedData: SavedFormulaData = JSON.parse(savedDataStr)
setSavedData(parsedData)
versionCounter.current = parsedData.version + 1
console.log(
// console.log(
'📂 Загружены сохраненные формулы (версия:',
parsedData.version,
')'
@@ -73,7 +73,7 @@ export const useFormulaAutoSave = (
const performSave = useCallback(
async (data: Record<string, Record<string, any>>) => {
if (Object.keys(data).length === 0) {
console.log('📭 Нет данных для сохранения')
// console.log('📭 Нет данных для сохранения')
return
}
@@ -110,7 +110,7 @@ export const useFormulaAutoSave = (
setSavedData(savedData)
setLastSavedRevision(revision)
console.log(
// console.log(
'💾 Сохранено формул:',
Object.values(data).reduce(
(sum, sheet) => sum + Object.keys(sheet).length,
@@ -136,7 +136,7 @@ export const useFormulaAutoSave = (
// Автосохранение с дебаунсом
const scheduleAutoSave = useCallback(() => {
console.log('⏰ scheduleAutoSave вызван, проверяем условия:', {
// console.log('⏰ scheduleAutoSave вызван, проверяем условия:', {
enableAutoSave,
isCalculating,
isSaving,
@@ -144,21 +144,21 @@ export const useFormulaAutoSave = (
})
if (!enableAutoSave || isCalculating || isSaving || !templateId) {
console.log('❌ Автосохранение отменено по условиям')
// console.log('❌ Автосохранение отменено по условиям')
return
}
// Очищаем предыдущий таймер
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
console.log('🔄 Очищен предыдущий таймер автосохранения')
// console.log('🔄 Очищен предыдущий таймер автосохранения')
}
console.log(`⏳ Запланировано автосохранение через ${autoSaveDelay}ms`)
// console.log(`⏳ Запланировано автосохранение через ${autoSaveDelay}ms`)
saveTimeoutRef.current = setTimeout(() => {
console.log('🎯 Выполняем отложенное автосохранение')
// console.log('🎯 Выполняем отложенное автосохранение')
const currentData = getAllModifiedCells()
console.log('📋 Данные для автосохранения:', currentData)
// console.log('📋 Данные для автосохранения:', currentData)
performSave(currentData)
}, autoSaveDelay)
}, [
@@ -173,7 +173,7 @@ export const useFormulaAutoSave = (
// Эффект для автосохранения при изменениях
useEffect(() => {
console.log('🔍 Проверка условий автосохранения:', {
// console.log('🔍 Проверка условий автосохранения:', {
enableAutoSave,
revision,
lastSavedRevision,
@@ -193,10 +193,10 @@ export const useFormulaAutoSave = (
!isCalculating &&
!isSaving
) {
console.log('✅ Планируем автосохранение...')
// console.log('✅ Планируем автосохранение...')
scheduleAutoSave()
} else {
console.log('⏸️ Автосохранение пропущено по условиям')
// console.log('⏸️ Автосохранение пропущено по условиям')
}
}, [
enableAutoSave,
@@ -209,20 +209,20 @@ export const useFormulaAutoSave = (
// Ручное сохранение
const manualSave = useCallback(() => {
console.log('🖱️ Ручное сохранение вызвано')
// console.log('🖱️ Ручное сохранение вызвано')
// Отменяем автосохранение если оно запланировано
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
saveTimeoutRef.current = null
console.log('⏹️ Отменено запланированное автосохранение')
// console.log('⏹️ Отменено запланированное автосохранение')
}
const currentData = getAllModifiedCells()
console.log('📋 Данные для сохранения:', currentData)
// console.log('📋 Данные для сохранения:', currentData)
if (Object.keys(currentData).length === 0) {
console.log('📭 Нет измененных данных для сохранения')
// console.log('📭 Нет измененных данных для сохранения')
return
}
@@ -243,7 +243,7 @@ export const useFormulaAutoSave = (
if (savedDataStr) {
const parsedData: SavedFormulaData = JSON.parse(savedDataStr)
console.log('📂 Формулы загружены из localStorage:', {
// console.log('📂 Формулы загружены из localStorage:', {
templateId,
version: parsedData.version,
timestamp: parsedData.timestamp,
@@ -273,7 +273,7 @@ export const useFormulaAutoSave = (
setLastSavedRevision(0)
versionCounter.current = 1
console.log('🗑️ Сохраненные формулы очищены:', { templateId })
// console.log('🗑️ Сохраненные формулы очищены:', { templateId })
} catch (error) {
console.error('❌ Ошибка при очистке сохраненных формул:', error)
}

View File

@@ -218,7 +218,7 @@ export function useMultiSheetEngine({
const setTemplateData = useCallback(
(data: Record<string, Record<string, any>>) => {
templateDataRef.current = JSON.parse(JSON.stringify(data)) // Глубокая копия
console.log('📋 Установлены данные шаблона:', templateDataRef.current)
// console.log('📋 Установлены данные шаблона:', templateDataRef.current)
},
[]
)
@@ -267,14 +267,14 @@ export function useMultiSheetEngine({
if (normalizedCurrent !== normalizedTemplate) {
// Если текущего значения нет (ячейка очищена), отправляем ''
modifiedSheet[cellAddress] = currentVal
console.log(
`🔧 Пользовательское изменение ${sheetName}:${cellAddress}:`,
{
template: normalizedTemplate,
current: normalizedCurrent,
value: currentVal,
}
)
// console.log(
// `🔧 Пользовательское изменение ${sheetName}:${cellAddress}:`,
// {
// template: normalizedTemplate,
// current: normalizedCurrent,
// value: currentVal,
// }
// )
}
})

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useDebounce } from 'use-debounce'
import {
ApiSheet,
createSheet,
@@ -12,41 +13,13 @@ export interface UseSheetAutoSaveOptions {
enableAutoSave?: boolean
}
/**
* Хук для автосохранения листов через API
*
* Функции:
* - Автоматическое сохранение с дебаунсом при изменениях
* - Ручное сохранение по требованию
* - Создание и обновление листов через API
* - Загрузка существующих листов
*/
// Утилита для извлечения UUID из templateId
const extractUUID = (id: string): string => {
// Если это уже чистый UUID, возвращаем как есть
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
if (uuidRegex.test(id)) {
return id
}
// Если есть префикс (например "template-"), извлекаем UUID
const match = id.match(
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
)
if (match) {
return match[0]
}
return id // Возвращаем оригинал если не найден UUID
}
export const useSheetAutoSave = (
getAllModifiedCells: () => Record<string, Record<string, any>>,
isCalculating: boolean,
revision: number,
options: UseSheetAutoSaveOptions = {}
) => {
console.log('getAllModifiedCells', getAllModifiedCells())
const { templateId, autoSaveDelay = 2000, enableAutoSave = true } = options
// Состояние
@@ -56,68 +29,40 @@ export const useSheetAutoSave = (
const [sheets, setSheets] = useState<Record<string, ApiSheet>>({})
const [isLoading, setIsLoading] = useState(false)
// Refs
const saveTimeoutRef = useRef<number | null>(null)
// Дебаунсинг ревизии для автосохранения
const [debouncedRevision] = useDebounce(revision, autoSaveDelay)
// Загрузка листов для шаблона при инициализации
const loadSheets = useCallback(async () => {
if (!templateId) return {}
const cleanTemplateId = extractUUID(templateId)
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
if (!uuidRegex.test(cleanTemplateId)) {
console.error('❌ Не удалось извлечь валидный UUID для загрузки:', {
original: templateId,
extracted: cleanTemplateId,
})
setLastSaveError('Неверный ID шаблона')
return {}
}
setIsLoading(true)
try {
const response = await getSheetsByTemplate(cleanTemplateId)
/*
* В API могут существовать листы как с «сырым» именем (Report/Calculations),
* так и уже нормализованные (L/R). Чтобы случайный порядок ответов не
* приводил к потере данных (один объект перезаписывал другой), собираем
* листы в map с объединением ячеек, а не простым overwrite.
*/
const sheetsMap = response.sheets.reduce(
(acc, sheet) => {
// Приводим названия к единым «L»/«R»
const normalizedName =
sheet.name === 'Report'
? 'L'
: sheet.name === 'Calculations'
? 'R'
: sheet.name
const response = await getSheetsByTemplate(templateId)
if (acc[normalizedName]) {
// Уже есть лист с таким именем — мерджим данные, приоритет у более
// свежего объекта (sheet), но оставляем существующие ячейки если их
// нет в новом
acc[normalizedName] = {
...sheet,
name: normalizedName,
const sheetsMap = response.sheets.reduce<Record<string, ApiSheet>>(
(acc, sheet) => {
const existingSheet = acc[sheet.name]
if (existingSheet) {
// Если лист уже есть, объединяем ячейки
acc[sheet.name] = {
...sheet, // берем актуальную версию листа
cells: {
...acc[normalizedName].cells,
...sheet.cells,
...existingSheet.cells, // старые ячейки
...sheet.cells, // новые заменяют старые с тем же ключом
},
}
} else {
acc[normalizedName] = { ...sheet, name: normalizedName }
acc[sheet.name] = { ...sheet }
}
return acc
},
{} as Record<string, ApiSheet>
{}
)
setSheets(sheetsMap)
console.log('📂 Загружены листы для шаблона:', Object.keys(sheetsMap))
return sheetsMap
} catch (error) {
console.error('❌ Ошибка при загрузке листов:', error)
@@ -138,71 +83,29 @@ export const useSheetAutoSave = (
// Основная функция сохранения через API
const performSave = useCallback(
async (data: Record<string, Record<string, any>>) => {
// Сохраняем все листы, но пропускаем пустые
const sheetNames = Object.keys(data)
if (sheetNames.length === 0) {
console.log('📭 Нет данных для сохранения')
return
}
if (!templateId) return
if (!templateId) {
console.warn('⚠️ Не указан templateId для сохранения листов')
return
}
// Получаем список всех листов, которые нужно обработать:
// 1. Листы с данными (из data)
// 2. Существующие листы, которые могли быть очищены
const dataSheetNames = Object.keys(data)
const existingSheetNames = Object.keys(sheets)
const allSheetNames = new Set([...dataSheetNames, ...existingSheetNames])
// Извлекаем и валидируем UUID из templateId
const cleanTemplateId = extractUUID(templateId)
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
if (!uuidRegex.test(cleanTemplateId)) {
console.error('❌ Не удалось извлечь валидный UUID из templateId:', {
original: templateId,
extracted: cleanTemplateId,
})
setLastSaveError('Неверный ID шаблона')
return
}
console.log('🧹 Очищенный templateId:', {
original: templateId,
clean: cleanTemplateId,
})
if (allSheetNames.size === 0) return
setIsSaving(true)
setLastSaveError(null)
try {
const savePromises = sheetNames.map(async sheetName => {
const sheetCells = data[sheetName]
const savePromises = Array.from(allSheetNames).map(async sheetName => {
const sheetCells = data[sheetName] || {} // если данных нет, используем пустой объект
const existingSheet = sheets[sheetName]
// Проверяем что есть данные для сохранения
if (!sheetCells || Object.keys(sheetCells).length === 0) {
console.log(`⏭️ Пропускаем лист "${sheetName}" - нет данных`)
return
}
if (existingSheet) {
// Объединяем старые и новые данные, чтобы не потерять предыдущие изменения
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: mergedCells,
cells: sheetCells,
})
// Обновляем локальное состояние
@@ -210,52 +113,41 @@ export const useSheetAutoSave = (
...prev,
[sheetName]: {
...existingSheet,
cells: mergedCells,
cells: sheetCells,
updated_at: new Date().toISOString(),
},
}))
} else {
// Создаем новый лист
console.log(`✨ Создаем новый лист "${sheetName}"`)
console.log('📋 Данные для создания:', {
name: sheetName,
cells: sheetCells,
template_id: cleanTemplateId,
cellsCount: Object.keys(sheetCells).length,
})
const newSheet = await createSheet({
name: sheetName,
cells: sheetCells,
template_id: cleanTemplateId,
})
// Создаем новый лист только если есть данные
if (Object.keys(sheetCells).length > 0) {
const newSheet = await createSheet({
name: sheetName,
cells: sheetCells,
template_id: templateId,
})
// Получаем полную информацию о созданном листе
const fullSheetData: ApiSheet = {
id: newSheet.id,
name: sheetName,
cells: sheetCells,
template_id: cleanTemplateId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
deleted_at: null,
// Получаем полную информацию о созданном листе
const fullSheetData: ApiSheet = {
id: newSheet.id,
name: sheetName,
cells: sheetCells,
template_id: templateId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
deleted_at: null,
}
// Обновляем локальное состояние
setSheets(prev => ({
...prev,
[sheetName]: fullSheetData,
}))
}
// Обновляем локальное состояние
setSheets(prev => ({
...prev,
[sheetName]: fullSheetData,
}))
}
})
await Promise.all(savePromises)
setLastSavedRevision(revision)
const totalCells = Object.values(data).reduce(
(sum, sheet) => sum + Object.keys(sheet).length,
0
)
console.log('💾 Сохранено ячеек:', totalCells, 'в листах:', sheetNames)
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Неизвестная ошибка'
@@ -268,98 +160,33 @@ export const useSheetAutoSave = (
[templateId, sheets, revision]
)
// Автосохранение с дебаунсом
const scheduleAutoSave = useCallback(() => {
console.log('⏰ scheduleAutoSave вызван, проверяем условия:', {
enableAutoSave,
isCalculating,
isSaving,
templateId,
})
// Автосохранение при изменении дебаунсированной ревизии
useEffect(() => {
const shouldAutoSave =
enableAutoSave &&
debouncedRevision > lastSavedRevision &&
!isCalculating &&
!isSaving &&
templateId
if (!enableAutoSave || isCalculating || isSaving || !templateId) {
console.log('Автосохранение отменено по условиям')
return
if (shouldAutoSave) {
console.log('✅ Выполняем автосохранение для ревизии:', debouncedRevision)
performSave(getAllModifiedCells())
}
// Очищаем предыдущий таймер
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
console.log('🔄 Очищен предыдущий таймер автосохранения')
}
console.log(`⏳ Запланировано автосохранение через ${autoSaveDelay}ms`)
saveTimeoutRef.current = setTimeout(() => {
console.log('🎯 Выполняем отложенное автосохранение')
const currentData = getAllModifiedCells()
console.log('📋 Данные для автосохранения:', currentData)
performSave(currentData)
}, autoSaveDelay)
}, [
enableAutoSave,
debouncedRevision,
lastSavedRevision,
isCalculating,
isSaving,
templateId,
autoSaveDelay,
getAllModifiedCells,
performSave,
])
// Эффект для автосохранения при изменениях
useEffect(() => {
console.log('🔍 Проверка условий автосохранения:', {
enableAutoSave,
revision,
lastSavedRevision,
isCalculating,
isSaving,
templateId,
shouldSave:
enableAutoSave &&
revision > lastSavedRevision &&
!isCalculating &&
!isSaving,
})
if (
enableAutoSave &&
revision > lastSavedRevision &&
!isCalculating &&
!isSaving
) {
console.log('✅ Планируем автосохранение...')
scheduleAutoSave()
} else {
console.log('⏸️ Автосохранение пропущено по условиям')
}
}, [
enableAutoSave,
revision,
lastSavedRevision,
isCalculating,
isSaving,
scheduleAutoSave,
])
// Ручное сохранение
const manualSave = useCallback(() => {
console.log('🖱️ Ручное сохранение вызвано')
// Отменяем автосохранение если оно запланировано
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
saveTimeoutRef.current = null
console.log('⏹️ Отменено запланированное автосохранение')
}
const currentData = getAllModifiedCells()
console.log('📋 Данные для сохранения:', currentData)
if (Object.keys(currentData).length === 0) {
console.log('📭 Нет измененных данных для сохранения')
return
}
performSave(currentData)
}, [getAllModifiedCells, performSave])
@@ -376,19 +203,9 @@ export const useSheetAutoSave = (
}
})
console.log('📂 Загружены сохраненные листы:', Object.keys(result))
return result
}, [sheets])
// Очистка таймаута при размонтировании
useEffect(() => {
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
}
}
}, [])
// Вычисляемые значения
const isDataChanged = revision > lastSavedRevision
const hasUnsavedChanges = isDataChanged && !isSaving

View File

@@ -20,28 +20,28 @@ export const DEFAULT_FUNCTIONS: Record<string, ExcelFunction> = {
const flatArgs = args.flat()
return flatArgs.reduce(
(sum, val) => sum + (typeof val === 'number' ? val : 0),
0,
0
)
},
СРЗНАЧ: (...args: any[]) => {
const flatArgs = args.flat()
const validArgs = flatArgs.filter((val) => typeof val === 'number')
const validArgs = flatArgs.filter(val => typeof val === 'number')
return validArgs.length > 0
? validArgs.reduce((sum, val) => sum + val, 0) / validArgs.length
: 0
},
СЧЁТ: (...args: any[]) => {
const flatArgs = args.flat()
return flatArgs.filter((val) => typeof val === 'number').length
return flatArgs.filter(val => typeof val === 'number').length
},
МАКС: (...args: any[]) => {
const flatArgs = args.flat()
const validArgs = flatArgs.filter((val) => typeof val === 'number')
const validArgs = flatArgs.filter(val => typeof val === 'number')
return validArgs.length > 0 ? Math.max(...validArgs) : 0
},
МИН: (...args: any[]) => {
const flatArgs = args.flat()
const validArgs = flatArgs.filter((val) => typeof val === 'number')
const validArgs = flatArgs.filter(val => typeof val === 'number')
return validArgs.length > 0 ? Math.min(...validArgs) : 0
},
ЕСЛИ: (условие: boolean, истина: CellValue, ложь: CellValue) =>
@@ -96,7 +96,7 @@ export const DEFAULT_FUNCTIONS: Record<string, ExcelFunction> = {
// Отладочная функция
ТЕСТ: (значение: any = 'Функция работает!') => {
console.log('ТЕСТ вызван с:', значение)
// console.log('ТЕСТ вызван с:', значение)
return значение
},
}

View File

@@ -143,9 +143,9 @@ export class Cell {
// Отладка
if (expr.includes('ТЕСТ')) {
console.log('Processing ТЕСТ function')
console.log('Available functions:', Object.keys(functions))
console.log('Original code:', code)
// console.log('Processing ТЕСТ function')
// console.log('Available functions:', Object.keys(functions))
// console.log('Original code:', code)
}
for (const [funcName] of Object.entries(functions)) {
@@ -164,12 +164,12 @@ export class Cell {
})
if (before !== code && expr.includes('ТЕСТ')) {
console.log(`Replaced ${funcName}:`, before, '->', code)
// console.log(`Replaced ${funcName}:`, before, '->', code)
}
}
if (expr.includes('ТЕСТ')) {
console.log('Final code:', code)
// console.log('Final code:', code)
}
// Создаём контекст для выполнения

View File

@@ -29,10 +29,10 @@ export const ElementsCreation: FC = () => {
}, [templateId, templates, navigate, isLoading])
const handleElementAdd = async (element: TemplateElement) => {
console.log('handleElementAdd called with element:', element)
// console.log('handleElementAdd called with element:', element)
if (!selectedTemplate) {
console.log('No selected template')
// console.log('No selected template')
return
}
@@ -44,8 +44,8 @@ export const ElementsCreation: FC = () => {
updatedAt: new Date(),
}
console.log('Updated template:', updatedTemplate)
console.log('Elements count:', updatedTemplate.elements.length)
// console.log('Updated template:', updatedTemplate)
// console.log('Elements count:', updatedTemplate.elements.length)
await updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate)

View File

@@ -441,15 +441,15 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
}
onSave(data)
console.log(
'[DBG] template elements',
template.elements.map(e => ({
id: e.id,
label: e.label,
target: e.targetCells,
type: e.type,
}))
)
// 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) {
@@ -569,7 +569,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
)
})
console.log('[DBG] Final cells_to_update', cellsToUpdate)
// console.log('[DBG] Final cells_to_update', cellsToUpdate)
// Получаем последний файл для шаблона
const latestFile = await getLatestFileForTemplate(template.id)
@@ -584,7 +584,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
template_id: template.id,
}
console.log('[DBG] POST body', body)
// console.log('[DBG] POST body', body)
const resp = await fetch('/api/protocols/', {
method: 'POST',
@@ -595,7 +595,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
const result = await resp.json()
console.log('✅ Протокол создан, ID:', result.protocol_id)
// console.log('✅ Протокол создан, ID:', result.protocol_id)
} catch (err) {
console.error('Ошибка при создании протокола', err)
alert('Ошибка при создании протокола')
@@ -603,7 +603,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
}
const handleExport = () => {
console.log('Экспорт в Excel:', { formData })
// console.log('Экспорт в Excel:', { formData })
alert('Функция экспорта будет реализована в следующих версиях')
}
@@ -767,7 +767,7 @@ export const ProtocolCreation: FC = () => {
}, [templateId, templates])
const handleProtocolSave = (data: Record<string, any>) => {
console.log('Сохранение протокола:', data)
// console.log('Сохранение протокола:', data)
// Здесь будет логика сохранения протокола
alert(`Протокол "${data.protocolName}" сохранен!`)
navigate('/templates')

View File

@@ -41,7 +41,7 @@ export async function createSheet(
sheet: CreateSheetInput
): Promise<CreateSheetOutput> {
try {
console.log('📤 Отправляем данные для создания листа:', sheet)
// console.log('📤 Отправляем данные для создания листа:', sheet)
const response = await fetch('/api/sheets/', {
method: 'POST',
@@ -65,7 +65,7 @@ export async function createSheet(
}
const result: CreateSheetOutput = await response.json()
console.log('✅ Лист создан успешно:', result)
// console.log('✅ Лист создан успешно:', result)
return result
} catch (error) {
console.error('Ошибка при создании листа:', error)
@@ -100,7 +100,7 @@ export async function getSheetsByTemplate(
templateId: string
): Promise<GetSheetsOutput> {
try {
console.log('📋 Запрашиваем листы для templateId:', templateId)
// console.log('📋 Запрашиваем листы для templateId:', templateId)
// Получаем все листы и фильтруем на клиенте
const response = await fetch(`/api/sheets/`, {
@@ -110,7 +110,7 @@ export async function getSheetsByTemplate(
},
})
console.log('📋 Ответ сервера на запрос листов:', response.status)
// console.log('📋 Ответ сервера на запрос листов:', response.status)
if (!response.ok) {
// Пытаемся получить подробности ошибки от сервера
@@ -134,12 +134,12 @@ export async function getSheetsByTemplate(
sheet => sheet.template_id === templateId
)
console.log('✅ Получены и отфильтрованы листы для шаблона:', {
templateId,
totalSheets: allSheets.sheets.length,
filteredSheetsCount: filteredSheets.length,
sheetNames: filteredSheets.map(s => s.name),
})
// console.log('✅ Получены и отфильтрованы листы для шаблона:', {
// templateId,
// totalSheets: allSheets.sheets.length,
// filteredSheetsCount: filteredSheets.length,
// sheetNames: filteredSheets.map(s => s.name),
// })
return { sheets: filteredSheets }
} catch (error) {
@@ -147,7 +147,7 @@ export async function getSheetsByTemplate(
// Если ошибка 404 или подобная, возвращаем пустой список вместо выброса ошибки
if (error instanceof Error && error.message.includes('404')) {
console.log('📭 Листы для шаблона не найдены, возвращаем пустой список')
// console.log('📭 Листы для шаблона не найдены, возвращаем пустой список')
return { sheets: [] }
}
@@ -179,10 +179,11 @@ export async function getSheet(sheetId: string): Promise<GetSheetOutput> {
// API обновление листа
export async function patchSheet(
sheetId: string,
sheetId: UUID,
sheet: PatchSheetInput
): Promise<PatchSheetOutput> {
try {
console.log('POST BODY:' + JSON.stringify(sheet))
const response = await fetch(`/api/sheets/${sheetId}`, {
method: 'PATCH',
headers: {

View File

@@ -26,114 +26,110 @@ const TemplateCard: React.FC<TemplateCardProps> = ({
const isConfigured = true // TODO: remove
return (
console.log(template.excelFile),
(
<Card
aria-label={`Шаблон ${template.name}`}
onClick={selectionMode ? () => onToggleSelect(template.id) : undefined}
className={clsx(
'group relative shadow-sm transition-all duration-200',
selectionMode && 'hover:cursor-pointer',
selectionMode &&
isSelected &&
'border border-primary ring-2 ring-primary ring-offset-1',
'hover:border-primary hover:shadow-lg'
)}
>
{selectionMode && (
<button
type="button"
tabIndex={0}
aria-pressed={isSelected}
aria-label={isSelected ? 'Снять выделение' : 'Выбрать'}
// console.log(template.excelFile),
<Card
aria-label={`Шаблон ${template.name}`}
onClick={selectionMode ? () => onToggleSelect(template.id) : undefined}
className={clsx(
'group relative shadow-sm transition-all duration-200',
selectionMode && 'hover:cursor-pointer',
selectionMode &&
isSelected &&
'border border-primary ring-2 ring-primary ring-offset-1',
'hover:border-primary hover:shadow-lg'
)}
>
{selectionMode && (
<button
type="button"
tabIndex={0}
aria-pressed={isSelected}
aria-label={isSelected ? 'Снять выделение' : 'Выбрать'}
onClick={e => {
e.stopPropagation()
onToggleSelect(template.id)
}}
className={clsx(
'absolute right-3 top-3 z-10 flex h-7 w-7 items-center justify-center rounded-full border border-border bg-white transition-all',
isSelected ? 'text-accent-foreground' : 'text-muted-foreground',
'focus-visible:ring-2 focus-visible:ring-accent'
)}
>
{isSelected ? <CheckCircle size={18} /> : <Circle size={18} />}
</button>
)}
<CardHeader className="pb-4">
<CardTitle className="text-base font-medium">{template.name}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col justify-between pt-0">
<div>
{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/80">
<span>
Создан:{' '}
{new Date(template.createdAt).toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: '2-digit',
})}
</span>
<span>
Обновлён:{' '}
{new Date(template.updatedAt).toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: '2-digit',
})}
</span>
</div>
</div>
<div className="mt-2 flex items-center gap-2">
<Button
size="sm"
className="flex-1"
disabled={selectionMode || !isConfigured}
onClick={e => {
e.stopPropagation()
onToggleSelect(template.id)
navigate(`/templates/${template.id}/protocols`)
}}
className={clsx(
'absolute right-3 top-3 z-10 flex h-7 w-7 items-center justify-center rounded-full border border-border bg-white transition-all',
isSelected ? 'text-accent-foreground' : 'text-muted-foreground',
'focus-visible:ring-2 focus-visible:ring-accent'
)}
>
{isSelected ? <CheckCircle size={18} /> : <Circle size={18} />}
</button>
)}
<CardHeader className="pb-4">
<CardTitle className="text-base font-medium">
{template.name}
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col justify-between pt-0">
<div>
{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/80">
<span>
Создан:{' '}
{new Date(template.createdAt).toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: '2-digit',
})}
</span>
<span>
Обновлён:{' '}
{new Date(template.updatedAt).toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: '2-digit',
})}
</span>
</div>
</div>
<div className="mt-2 flex items-center gap-2">
<Button
size="sm"
className="flex-1"
disabled={selectionMode || !isConfigured}
onClick={e => {
e.stopPropagation()
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"
disabled={selectionMode}
onClick={e => {
e.stopPropagation()
navigate(`/templates/${template.id}/edit`)
}}
aria-label="Редактировать шаблон"
>
<Settings className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="outline"
className="h-8 w-8 p-0"
disabled={selectionMode}
onClick={e => {
e.stopPropagation()
navigate(`/templates/${template.id}/elements`)
}}
aria-label="Настроить элементы"
>
<Wrench className="h-3 w-3" />
</Button>
</div>
</CardContent>
</Card>
)
<FileText className="mr-2 h-3 w-3" />
{isConfigured ? 'Создать протокол' : 'Требует настройки'}
</Button>
<Button
size="sm"
variant="outline"
className="h-8 w-8 p-0"
disabled={selectionMode}
onClick={e => {
e.stopPropagation()
navigate(`/templates/${template.id}/edit`)
}}
aria-label="Редактировать шаблон"
>
<Settings className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="outline"
className="h-8 w-8 p-0"
disabled={selectionMode}
onClick={e => {
e.stopPropagation()
navigate(`/templates/${template.id}/elements`)
}}
aria-label="Настроить элементы"
>
<Wrench className="h-3 w-3" />
</Button>
</div>
</CardContent>
</Card>
)
}