-
-
- Excel шаблон:
-
- {editedTemplate.excelFile ? (
-
-
-
- {editedTemplate.excelFile.name}
-
-
- ) : (
-
- Файл не загружен
-
- )}
+ {/* Левая часть - панель инструментов стилизации */}
+
+ {/* Группа текстового форматирования */}
+
+
+
+
+
+
+
+
+ {/* Группа выравнивания */}
+
+
+
+
+ {/* Группа цвета и стилей */}
+
-
+
+ {/* Правая часть - загрузка Excel */}
+
= ({
onChange={handleFileUpload}
className="hidden"
/>
+
+ {/* Статус файла - всегда отображается */}
+
+
+
+ {editedTemplate.excelFile
+ ? editedTemplate.excelFile.name
+ : 'Файл не выбран'}
+
+
+
+ {/* Кнопка загрузки - всегда отображается */}
@@ -244,6 +305,7 @@ export const TemplateEditor: React.FC
= ({
key={dataVersion}
templateData={{ L: excelData }}
mergedCells={editedTemplate.mergedCells}
+ templateId={`template-${editedTemplate.id}`}
/>
diff --git a/src/components/TemplateManager/TemplateManager.tsx b/src/components/TemplateManager/TemplateManager.tsx
index 4555833..b82a0d1 100644
--- a/src/components/TemplateManager/TemplateManager.tsx
+++ b/src/components/TemplateManager/TemplateManager.tsx
@@ -1,27 +1,62 @@
-import { FileText, Plus, Settings, Upload } from 'lucide-react';
-import React, { useState } from 'react';
-import { useTemplateContext } from '../../contexts/TemplateContext';
-import { Template } from '../../types/template';
-import { Button } from '../ui/button';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
-import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
-import { Input } from '../ui/input';
-import { TemplateEditor } from './TemplateEditor';
+import { FileText, Plus, Settings, Trash2, Wrench } from 'lucide-react'
+import React, { useEffect, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { useTemplateContext } from '../../contexts/TemplateContext'
+import { Template } from '../../types/template'
+import { Button } from '../ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '../ui/card'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '../ui/dialog'
+import { Input } from '../ui/input'
+import { TemplateEditor } from './TemplateEditor'
interface TemplateManagerProps {
- onTemplateSelect?: (template: Template) => void;
+ onTemplateSelect?: (template: Template) => void
+ templateId?: string
}
-export const TemplateManager: React.FC
= () => {
- const { templates, addTemplate, updateTemplate } = useTemplateContext();
- const [selectedTemplate, setSelectedTemplate] = useState(null);
- const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
- const [isEditMode, setIsEditMode] = useState(false);
- const [newTemplateName, setNewTemplateName] = useState('');
- const [newTemplateDescription, setNewTemplateDescription] = useState('');
+export const TemplateManager: React.FC = ({
+ templateId,
+}) => {
+ const navigate = useNavigate()
+ const { templates, addTemplate, updateTemplate, deleteTemplate } =
+ useTemplateContext()
+ const [selectedTemplate, setSelectedTemplate] = useState(
+ null,
+ )
+ const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
+ const [isEditMode, setIsEditMode] = useState(false)
+ const [newTemplateName, setNewTemplateName] = useState('')
+ const [newTemplateDescription, setNewTemplateDescription] = useState('')
+ const [templateToDelete, setTemplateToDelete] = useState(
+ null,
+ )
+
+ // Автоматически открываем редактирование шаблона, если передан templateId
+ useEffect(() => {
+ if (templateId) {
+ const template = templates.find((t) => t.id === templateId)
+ if (template) {
+ setSelectedTemplate(template)
+ setIsEditMode(true)
+ }
+ }
+ }, [templateId, templates])
const handleCreateTemplate = () => {
- if (!newTemplateName.trim()) return;
+ if (!newTemplateName.trim()) return
const newTemplate: Template = {
id: Date.now().toString(),
@@ -30,67 +65,54 @@ export const TemplateManager: React.FC = () => {
elements: [],
createdAt: new Date(),
updatedAt: new Date(),
- };
+ }
- addTemplate(newTemplate);
- setSelectedTemplate(newTemplate);
- setIsEditMode(true);
- setIsCreateDialogOpen(false);
- setNewTemplateName('');
- setNewTemplateDescription('');
- };
+ addTemplate(newTemplate)
+ setSelectedTemplate(newTemplate)
+ setIsEditMode(true)
+ setIsCreateDialogOpen(false)
+ setNewTemplateName('')
+ setNewTemplateDescription('')
+ }
const handleTemplateUpdate = (updatedTemplate: Template) => {
- updateTemplate(updatedTemplate);
- setSelectedTemplate(updatedTemplate);
- };
+ updateTemplate(updatedTemplate)
+ setSelectedTemplate(updatedTemplate)
+ }
- // const handleTemplateDelete = (templateId: string) => {
- // deleteTemplate(templateId);
- // if (selectedTemplate?.id === templateId) {
- // setSelectedTemplate(null);
- // setIsEditMode(false);
- // }
- // };
-
- // const handleTemplateSelect = (template: Template) => {
- // setSelectedTemplate(template);
- // setIsEditMode(false);
- // onTemplateSelect?.(template);
- // };
-
- const handleEditTemplate = (template: Template) => {
- setSelectedTemplate(template);
- setIsEditMode(true);
- };
+ const handleDeleteTemplate = (template: Template) => {
+ deleteTemplate(template.id)
+ setTemplateToDelete(null)
+ }
if (selectedTemplate && isEditMode) {
return (
setIsEditMode(false)}
onBack={() => {
- setSelectedTemplate(null);
- setIsEditMode(false);
+ setSelectedTemplate(null)
+ setIsEditMode(false)
+ navigate('/templates')
}}
/>
- );
+ )
}
return (
-
-
+
+
-
Настройка шаблонов
-
Создание и управление шаблонами протоколов
+
Protoc
+
+ Создание протоколов с помощью шаблонов
+
-
+
-
+
= () => {
-
-
{templates.length === 0 ? (
-
-
-
Нет созданных шаблонов
-
Создайте первый шаблон для начала работы
+
+
+
+ Нет созданных шаблонов
+
+
+ Создайте первый шаблон для начала работы
+
) : (
-
- {templates.map((template) => (
-
-
-
-
- {template.name}
- {template.description && (
-
- {template.description}
-
- )}
-
-
+
+ {templates.map((template) => {
+ return (
+
+
+
+
+
+ {template.name}
+
+ {template.description && (
+
+ {template.description}
+
+ )}
+
-
-
-
-
-
-
-
- {template.excelFile ? 'Excel файл загружен' : 'Excel файл не загружен'}
-
+
+
+ Создан: {template.createdAt.toLocaleDateString('ru-RU')}
+
+
+ Обновлен: {template.updatedAt.toLocaleDateString('ru-RU')}
+
-
-
-
{template.elements.length} элементов интерфейса
+
+
+
+
+
+ {template.excelFile ? '✓' : '✗'}
+
+
+
+ {template.elements.length}
+
-
- Создан: {template.createdAt.toLocaleDateString('ru-RU')}
+
+
+
+
+
+
+
+
-
- {/*
-
-
*/}
-
-
- ))}
+
+
+ )
+ })}
)}
+
+ {/* Диалог подтверждения удаления */}
+
- );
-};
\ No newline at end of file
+ )
+}
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
index bb6f6a2..21c5039 100644
--- a/src/components/ui/badge.tsx
+++ b/src/components/ui/badge.tsx
@@ -1,27 +1,27 @@
-import { cva, type VariantProps } from "class-variance-authority";
-import * as React from "react";
+import { cva, type VariantProps } from 'class-variance-authority'
+import * as React from 'react'
-import { cn } from "@/lib/utils";
+import { cn } from '../../lib/utils'
const badgeVariants = cva(
- "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
- "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
- "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
- "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
- outline: "text-foreground",
+ 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
+ outline: 'text-foreground',
},
},
defaultVariants: {
- variant: "default",
+ variant: 'default',
},
- }
-);
+ },
+)
export interface BadgeProps
extends React.HTMLAttributes
,
@@ -30,7 +30,7 @@ export interface BadgeProps
function Badge({ className, variant, ...props }: BadgeProps) {
return (
- );
+ )
}
-export { Badge, badgeVariants };
+export { Badge, badgeVariants }
diff --git a/src/contexts/TemplateContext.tsx b/src/contexts/TemplateContext.tsx
index 2d18c00..9512a0c 100644
--- a/src/contexts/TemplateContext.tsx
+++ b/src/contexts/TemplateContext.tsx
@@ -1,29 +1,40 @@
-import React, { createContext, ReactNode, useContext, useState } from 'react';
-import { getDefaultLayoutSettings, migrateTemplateElement } from '../lib/utils';
-import { Template, TemplateElement } from '../types/template';
+import React, {
+ createContext,
+ ReactNode,
+ useContext,
+ useEffect,
+ useState,
+} from 'react'
+import { getDefaultLayoutSettings, migrateTemplateElement } from '../lib/utils'
+import { Template, TemplateElement } from '../types/template'
interface TemplateContextType {
- templates: Template[];
- addTemplate: (template: Template) => void;
- updateTemplate: (template: Template) => void;
- deleteTemplate: (templateId: string) => void;
- getTemplate: (templateId: string) => Template | undefined;
+ templates: Template[]
+ addTemplate: (template: Template) => void
+ updateTemplate: (template: Template) => void
+ deleteTemplate: (templateId: string) => void
+ getTemplate: (templateId: string) => Template | undefined
}
-const TemplateContext = createContext(undefined);
+const TemplateContext = createContext(
+ undefined,
+)
export const useTemplateContext = () => {
- const context = useContext(TemplateContext);
+ const context = useContext(TemplateContext)
if (!context) {
- throw new Error('useTemplateContext must be used within a TemplateProvider');
+ throw new Error('useTemplateContext must be used within a TemplateProvider')
}
- return context;
-};
+ return context
+}
interface TemplateProviderProps {
- children: ReactNode;
+ children: ReactNode
}
+// Ключ для localStorage
+const TEMPLATES_STORAGE_KEY = 'protoc_templates'
+
// Функция создания элемента названия протокола
function createProtocolNameElement(): TemplateElement {
return {
@@ -33,7 +44,9 @@ function createProtocolNameElement(): TemplateElement {
label: 'Название протокола',
placeholder: 'Введите название протокола',
required: true,
- targetCells: [{ sheet: 'L', cell: 'A1', displayName: 'Название протокола' }],
+ targetCells: [
+ { sheet: 'L', cell: 'A1', displayName: 'Название протокола' },
+ ],
options: [],
order: 0,
layout: {
@@ -43,7 +56,7 @@ function createProtocolNameElement(): TemplateElement {
height: 80,
zIndex: 1,
},
- };
+ }
}
// Функция миграции шаблона
@@ -52,63 +65,106 @@ function migrateTemplate(template: any): Template {
...template,
elements: template.elements?.map(migrateTemplateElement) || [],
layoutSettings: template.layoutSettings || getDefaultLayoutSettings(),
- };
+ createdAt: template.createdAt ? new Date(template.createdAt) : new Date(),
+ updatedAt: template.updatedAt ? new Date(template.updatedAt) : new Date(),
+ }
}
-export const TemplateProvider: React.FC = ({ children }) => {
- const [templates, setTemplates] = useState([]);
+// Функция загрузки шаблонов из localStorage
+function loadTemplatesFromStorage(): Template[] {
+ try {
+ const stored = localStorage.getItem(TEMPLATES_STORAGE_KEY)
+ if (stored) {
+ const parsed = JSON.parse(stored)
+ return Array.isArray(parsed) ? parsed.map(migrateTemplate) : []
+ }
+ } catch (error) {
+ console.error('Ошибка при загрузке шаблонов из localStorage:', error)
+ }
+ return []
+}
+
+// Функция сохранения шаблонов в localStorage
+function saveTemplatesToStorage(templates: Template[]): void {
+ try {
+ localStorage.setItem(TEMPLATES_STORAGE_KEY, JSON.stringify(templates))
+ } catch (error) {
+ console.error('Ошибка при сохранении шаблонов в localStorage:', error)
+ }
+}
+
+export const TemplateProvider: React.FC = ({
+ children,
+}) => {
+ const [templates, setTemplates] = useState(() => {
+ // Загружаем шаблоны из localStorage при инициализации
+ return loadTemplatesFromStorage()
+ })
+
+ // Сохраняем шаблоны в localStorage при каждом изменении
+ useEffect(() => {
+ saveTemplatesToStorage(templates)
+ }, [templates])
const addTemplate = (template: Template) => {
// Проверяем есть ли уже элемент названия протокола
- const hasProtocolName = template.elements.some(el =>
- el.name === 'protocolName' || el.label === 'Название протокола'
- );
-
- let templateWithProtocolName = template;
+ const hasProtocolName = template.elements.some(
+ (el) => el.name === 'protocolName' || el.label === 'Название протокола',
+ )
+
+ let templateWithProtocolName = template
if (!hasProtocolName) {
// Добавляем элемент названия протокола как первый элемент
- const protocolNameElement = createProtocolNameElement();
+ const protocolNameElement = createProtocolNameElement()
templateWithProtocolName = {
...template,
- elements: [protocolNameElement, ...template.elements.map(el => ({ ...el, order: (el.order || 0) + 1 }))]
- };
+ elements: [
+ protocolNameElement,
+ ...template.elements.map((el) => ({
+ ...el,
+ order: (el.order || 0) + 1,
+ })),
+ ],
+ }
}
-
- const migratedTemplate = migrateTemplate(templateWithProtocolName);
- setTemplates(prev => [...prev, migratedTemplate]);
- };
+
+ const migratedTemplate = migrateTemplate(templateWithProtocolName)
+ setTemplates((prev) => [...prev, migratedTemplate])
+ }
const updateTemplate = (updatedTemplate: Template) => {
- console.log('updateTemplate called with:', updatedTemplate);
- console.log('Elements count:', updatedTemplate.elements.length);
-
- const migratedTemplate = migrateTemplate(updatedTemplate);
- console.log('Migrated template:', migratedTemplate);
- console.log('Migrated elements count:', migratedTemplate.elements.length);
-
- setTemplates(prev => prev.map(t =>
- t.id === migratedTemplate.id ? migratedTemplate : t
- ));
- };
+ console.log('updateTemplate called with:', updatedTemplate)
+ console.log('Elements count:', updatedTemplate.elements.length)
+
+ const migratedTemplate = migrateTemplate(updatedTemplate)
+ console.log('Migrated template:', migratedTemplate)
+ console.log('Migrated elements count:', migratedTemplate.elements.length)
+
+ setTemplates((prev) =>
+ prev.map((t) => (t.id === migratedTemplate.id ? migratedTemplate : t)),
+ )
+ }
const deleteTemplate = (templateId: string) => {
- setTemplates(prev => prev.filter(t => t.id !== templateId));
- };
+ setTemplates((prev) => prev.filter((t) => t.id !== templateId))
+ }
const getTemplate = (templateId: string) => {
- const template = templates.find(t => t.id === templateId);
- return template ? migrateTemplate(template) : undefined;
- };
+ const template = templates.find((t) => t.id === templateId)
+ return template ? migrateTemplate(template) : undefined
+ }
return (
-
+
{children}
- );
-};
\ No newline at end of file
+ )
+}
diff --git a/src/hooks/useFormulaAutoSave.ts b/src/hooks/useFormulaAutoSave.ts
new file mode 100644
index 0000000..2cfd7c8
--- /dev/null
+++ b/src/hooks/useFormulaAutoSave.ts
@@ -0,0 +1,313 @@
+import { useCallback, useEffect, useRef, useState } from 'react'
+
+export interface SavedFormulaData {
+ templateId?: string
+ formulaData: Record>
+ timestamp: string
+ version: number
+}
+
+export interface UseFormulaAutoSaveOptions {
+ templateId?: string
+ autoSaveDelay?: number // задержка автосохранения в мс
+ enableAutoSave?: boolean
+}
+
+// Ключ для localStorage
+const getStorageKey = (templateId: string) => {
+ console.log('🔑 Получен templateId:', templateId)
+ return `formula_data_${templateId}`
+}
+
+/**
+ * Хук для автосохранения формул электронных таблиц в localStorage
+ *
+ * Функции:
+ * - Автоматическое сохранение с дебаунсом при изменениях
+ * - Ручное сохранение по требованию
+ * - Отслеживание состояния сохранения
+ * - Сохранение в localStorage (заглушка для API)
+ */
+export const useFormulaAutoSave = (
+ getAllModifiedCells: () => Record>,
+ isCalculating: boolean,
+ revision: number,
+ options: UseFormulaAutoSaveOptions = {},
+) => {
+ const { templateId, autoSaveDelay = 2000, enableAutoSave = true } = options
+
+ // Состояние
+ const [lastSavedRevision, setLastSavedRevision] = useState(0)
+ const [isSaving, setIsSaving] = useState(false)
+ const [lastSaveError, setLastSaveError] = useState(null)
+ const [savedData, setSavedData] = useState(null)
+
+ // Refs
+ const saveTimeoutRef = useRef(null)
+ const versionCounter = useRef(1)
+
+ // Загрузка данных из localStorage при инициализации
+ useEffect(() => {
+ if (!templateId) return
+
+ try {
+ const storageKey = getStorageKey(templateId)
+ const savedDataStr = localStorage.getItem(storageKey)
+
+ if (savedDataStr) {
+ const parsedData: SavedFormulaData = JSON.parse(savedDataStr)
+ setSavedData(parsedData)
+ versionCounter.current = parsedData.version + 1
+ console.log(
+ '📂 Загружены сохраненные формулы (версия:',
+ parsedData.version,
+ ')',
+ )
+ }
+ } catch (error) {
+ console.warn('⚠️ Ошибка при загрузке данных из localStorage:', error)
+ }
+ }, [templateId])
+
+ // Основная функция сохранения в localStorage
+ const performSave = useCallback(
+ async (data: Record>) => {
+ if (Object.keys(data).length === 0) {
+ console.log('📭 Нет данных для сохранения')
+ return
+ }
+
+ if (!templateId) {
+ console.warn('⚠️ Не указан templateId для сохранения формул')
+ return
+ }
+
+ setIsSaving(true)
+ setLastSaveError(null)
+
+ try {
+ // Имитируем задержку API для реалистичности
+ await new Promise((resolve) =>
+ setTimeout(resolve, 200 + Math.random() * 300),
+ )
+
+ // Иногда имитируем ошибку (2% вероятность)
+ if (Math.random() < 0.02) {
+ throw new Error('Ошибка записи в хранилище')
+ }
+
+ const savedData: SavedFormulaData = {
+ templateId,
+ formulaData: data,
+ timestamp: new Date().toISOString(),
+ version: versionCounter.current++,
+ }
+
+ // Сохраняем в localStorage
+ const storageKey = getStorageKey(templateId)
+ localStorage.setItem(storageKey, JSON.stringify(savedData))
+
+ setSavedData(savedData)
+ setLastSavedRevision(revision)
+
+ console.log(
+ '💾 Сохранено формул:',
+ Object.values(data).reduce(
+ (sum, sheet) => sum + Object.keys(sheet).length,
+ 0,
+ ),
+ )
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Неизвестная ошибка'
+ setLastSaveError(errorMessage)
+ console.error(
+ '❌ Ошибка при сохранении формул в localStorage:',
+ errorMessage,
+ )
+
+ // Не обновляем lastSavedRevision при ошибке, чтобы попытаться снова
+ } finally {
+ setIsSaving(false)
+ }
+ },
+ [templateId, revision],
+ )
+
+ // Автосохранение с дебаунсом
+ const scheduleAutoSave = useCallback(() => {
+ console.log('⏰ scheduleAutoSave вызван, проверяем условия:', {
+ enableAutoSave,
+ isCalculating,
+ isSaving,
+ templateId,
+ })
+
+ if (!enableAutoSave || isCalculating || isSaving || !templateId) {
+ console.log('❌ Автосохранение отменено по условиям')
+ return
+ }
+
+ // Очищаем предыдущий таймер
+ 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,
+ 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])
+
+ // Функция загрузки сохраненных формул из localStorage
+ const loadSavedFormulas = useCallback((): Record<
+ string,
+ Record
+ > | null => {
+ if (!templateId) return null
+
+ try {
+ const storageKey = getStorageKey(templateId)
+ const savedDataStr = localStorage.getItem(storageKey)
+
+ if (savedDataStr) {
+ const parsedData: SavedFormulaData = JSON.parse(savedDataStr)
+
+ console.log('📂 Формулы загружены из localStorage:', {
+ templateId,
+ version: parsedData.version,
+ timestamp: parsedData.timestamp,
+ })
+
+ return parsedData.formulaData
+ }
+
+ return null
+ } catch (error) {
+ console.error(
+ '❌ Ошибка при загрузке сохраненных формул из localStorage:',
+ error,
+ )
+ return null
+ }
+ }, [templateId])
+
+ // Функция очистки сохраненных данных
+ const clearSavedFormulas = useCallback(() => {
+ if (!templateId) return
+
+ try {
+ const storageKey = getStorageKey(templateId)
+ localStorage.removeItem(storageKey)
+ setSavedData(null)
+ setLastSavedRevision(0)
+ versionCounter.current = 1
+
+ console.log('🗑️ Сохраненные формулы очищены:', { templateId })
+ } catch (error) {
+ console.error('❌ Ошибка при очистке сохраненных формул:', error)
+ }
+ }, [templateId])
+
+ // Очистка таймаута при размонтировании
+ useEffect(() => {
+ return () => {
+ if (saveTimeoutRef.current) {
+ clearTimeout(saveTimeoutRef.current)
+ }
+ }
+ }, [])
+
+ // Вычисляемые значения
+ const isDataChanged = revision > lastSavedRevision
+ const hasUnsavedChanges = isDataChanged && !isSaving
+
+ return {
+ // Состояние
+ isSaving,
+ isDataChanged,
+ hasUnsavedChanges,
+ lastSaveError,
+ lastSavedRevision,
+ savedData,
+
+ // Методы
+ manualSave,
+ loadSavedFormulas,
+ clearSavedFormulas,
+
+ // Утилиты
+ getCurrentData: getAllModifiedCells,
+ clearError: () => setLastSaveError(null),
+ }
+}
diff --git a/src/lib/spreadsheet-engine/spreadsheet-engine.ts b/src/lib/spreadsheet-engine/spreadsheet-engine.ts
index 7abb884..bd429cd 100644
--- a/src/lib/spreadsheet-engine/spreadsheet-engine.ts
+++ b/src/lib/spreadsheet-engine/spreadsheet-engine.ts
@@ -157,7 +157,7 @@ export class Cell {
'gi',
)
const before = code
- code = code.replace(funcRegex, (match, p1, offset) => {
+ code = code.replace(funcRegex, (match, p1) => {
// p1 - это название функции, нужно сохранить символ перед функцией если он есть
const prefix = match.substring(0, match.indexOf(p1))
return `${prefix}__functions__.${funcName}(`
diff --git a/src/pages/ElementsCreation/ui/Page/Page.tsx b/src/pages/ElementsCreation/ui/Page/Page.tsx
index 1032a6c..4f379d1 100644
--- a/src/pages/ElementsCreation/ui/Page/Page.tsx
+++ b/src/pages/ElementsCreation/ui/Page/Page.tsx
@@ -1,6 +1,6 @@
import { Plus, Wrench } from 'lucide-react'
-import { FC, useState } from 'react'
-import { useNavigate } from 'react-router-dom'
+import { FC, useEffect, useState } from 'react'
+import { useNavigate, useParams } from 'react-router-dom'
import { ElementConstructor } from '../../../../components/TemplateManager/ElementConstructor'
import { Button } from '../../../../components/ui/button'
import {
@@ -19,11 +19,25 @@ import {
export const ElementsCreation: FC = () => {
const navigate = useNavigate()
+ const { templateId } = useParams<{ templateId: string }>()
const { templates, updateTemplate } = useTemplateContext()
const [selectedTemplate, setSelectedTemplate] = useState(
null,
)
+ // Автоматически выбираем шаблон, если передан templateId
+ useEffect(() => {
+ if (templateId) {
+ const template = templates.find((t) => t.id === templateId)
+ if (template) {
+ setSelectedTemplate(template)
+ } else {
+ // Если шаблон не найден, перенаправляем на страницу со списком шаблонов
+ navigate('/templates', { replace: true })
+ }
+ }
+ }, [templateId, templates, navigate])
+
const handleTemplateSelect = (template: Template) => {
setSelectedTemplate(template)
}
@@ -112,10 +126,6 @@ export const ElementsCreation: FC = () => {
setSelectedTemplate(updatedTemplate)
}
- const handleBack = () => {
- setSelectedTemplate(null)
- }
-
// Настройки отображения по умолчанию
const defaultLayoutSettings: FormLayoutSettings = {
gridSize: 20,
@@ -128,7 +138,7 @@ export const ElementsCreation: FC = () => {
-