ваше сообщение коммита
This commit is contained in:
129
RAG_TASKS.md
Normal file
129
RAG_TASKS.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Внедрение RAG-ассистента: поэтапный план
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Особенности проекта: разнообразие клиентов, каналов и данных
|
||||||
|
|
||||||
|
- **Клиенты:**
|
||||||
|
- Различные сегменты: B2B, B2C, VIP, оптовые и розничные покупатели, корпоративные клиенты, частные лица и др.
|
||||||
|
- Различные сценарии взаимодействия (покупка, поддержка, консультация, возврат и т.д.).
|
||||||
|
|
||||||
|
- **Каналы коммуникации:**
|
||||||
|
- Веб-чат
|
||||||
|
- Email
|
||||||
|
- Telegram/мессенджеры
|
||||||
|
- Возможна интеграция с другими каналами (WhatsApp, телефон и др.)
|
||||||
|
|
||||||
|
- **Типы данных:**
|
||||||
|
- Текстовые сообщения
|
||||||
|
- Аудио, видео, изображения (мультимодальные данные)
|
||||||
|
- Вложения (документы, сканы, фото товаров и т.д.)
|
||||||
|
|
||||||
|
- **Языки:**
|
||||||
|
- Русский
|
||||||
|
- Английский
|
||||||
|
- Испанский
|
||||||
|
- Китайский
|
||||||
|
- Возможность расширения на другие языки
|
||||||
|
|
||||||
|
- **Товары и услуги:**
|
||||||
|
- Широкий ассортимент товаров (разные категории, бренды, характеристики)
|
||||||
|
- Различные услуги (консультации, сервис, доставка, гарантия, возврат и др.)
|
||||||
|
- Возможность кросс-продаж и рекомендаций
|
||||||
|
|
||||||
|
- **Требования к RAG:**
|
||||||
|
- Гибкая фильтрация знаний по сегменту клиента, языку, категории товара/услуги, каналу обращения
|
||||||
|
- Поддержка мультиязычности и мультимодальности
|
||||||
|
- Масштабируемость для добавления новых ассистентов, сегментов, каналов и языков
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этап 1. Проектирование и подготовка инфраструктуры
|
||||||
|
1. **Проектирование схемы хранения знаний (RAG):**
|
||||||
|
- Описать структуру таблицы `knowledge_documents` (миграция).
|
||||||
|
- Определить поля: id, content, language, type (текст/медиа), метаданные, дата, автор и т.д.
|
||||||
|
2. **Подготовка backend:**
|
||||||
|
- Создать миграцию и модель для `knowledge_documents`.
|
||||||
|
- Подготовить базовые CRUD-эндпоинты для работы с базой знаний.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этап 2. Интеграция векторного поиска (RAG)
|
||||||
|
1. **Реализация векторного хранилища:**
|
||||||
|
- Реализовать методы инициализации и поиска (`initVectorStore`, `findSimilarDocuments`) в `ai-assistant.js`.
|
||||||
|
- Настроить хранение эмбеддингов для документов.
|
||||||
|
2. **API для поиска знаний:**
|
||||||
|
- Добавить эндпоинт для поиска релевантных знаний по запросу пользователя.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этап 3. Интеграция RAG в pipeline ассистента
|
||||||
|
1. **Модификация логики ответа ассистента:**
|
||||||
|
- При получении сообщения пользователя — искать релевантные знания и включать их в prompt LLM.
|
||||||
|
- Обеспечить мультиязычность поиска и генерации ответа.
|
||||||
|
2. **Логирование и трассировка:**
|
||||||
|
- Сохранять, какие знания были использованы для ответа.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этап 4. Интерфейс для админа
|
||||||
|
1. **UI для управления знаниями:**
|
||||||
|
- Добавить на фронте раздел для просмотра, добавления, редактирования и удаления знаний.
|
||||||
|
2. **UI для модерации ответов ассистента:**
|
||||||
|
- Кнопки "Редактировать", "Отправить", "Добавить в RAG" для сообщений и ответов.
|
||||||
|
- Возможность быстро добавить сообщение пользователя или ответ ассистента в базу знаний.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этап 5. Поддержка мультимодальности и мультиязычности
|
||||||
|
1. **Обработка вложений (аудио, видео, картинки):**
|
||||||
|
- Решить, как хранить и индексировать такие данные (например, хранить ссылки и метаданные, а не сами файлы).
|
||||||
|
2. **Мультиязычный поиск и генерация:**
|
||||||
|
- Проверить корректность работы эмбеддингов и LLM для разных языков.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этап 6. Тестирование и оптимизация
|
||||||
|
1. **Покрытие тестами ключевых сценариев (unit, интеграционные).**
|
||||||
|
2. **Оптимизация скорости поиска и генерации.**
|
||||||
|
3. **Документация для команды.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Бизнес-логика управления знаниями и тегами для RAG-ассистента
|
||||||
|
|
||||||
|
### 1. Гибкая система тегов и связей с пользователями
|
||||||
|
- Пользователь может создавать собственные таблицы тегов (например, "покупатель", "поставщик", "VIP-клиент" и т.д.).
|
||||||
|
- В таблице тегов должна быть возможность добавлять ссылки (relation) на пользователей из таблицы `users`.
|
||||||
|
- Для одного тега может быть привязано несколько пользователей (мультисвязь).
|
||||||
|
- Для одного пользователя может быть несколько тегов.
|
||||||
|
|
||||||
|
### 2. Управление знаниями (FAQ, инструкции, ответы)
|
||||||
|
- Пользователь может создавать таблицы с вопросами и ответами (например, FAQ для определённой группы клиентов).
|
||||||
|
- Каждая запись (вопрос-ответ) может быть связана с определённым тегом или группой тегов.
|
||||||
|
- Возможна фильтрация и поиск знаний по тегам, языку, типу клиента и другим параметрам.
|
||||||
|
|
||||||
|
### 3. Использование тегов и знаний в RAG-ассистенте
|
||||||
|
- При обработке запроса пользователя RAG-ассистент определяет его теги (по связям в таблице тегов).
|
||||||
|
- Для генерации ответа ассистент использует только те знания (вопросы/ответы), которые соответствуют тегам пользователя.
|
||||||
|
- Администратор может добавлять новые теги, связывать их с пользователями, а также создавать и редактировать знания для каждой группы.
|
||||||
|
|
||||||
|
### 4. UI/UX требования
|
||||||
|
- В интерфейсе создания/редактирования пользовательских таблиц должен быть доступен тип столбца "relation" (связь с users).
|
||||||
|
- Для ячеек типа "relation" реализовать выпадающий список с поиском по пользователям.
|
||||||
|
- Для таблиц знаний — возможность выбора одного или нескольких тегов для каждой записи.
|
||||||
|
|
||||||
|
**Пример структуры:**
|
||||||
|
- Таблица `user_tags`: id, name, [user_id (relation, мультисвязь)]
|
||||||
|
- Таблица `faq`: id, question, answer, [tag_id (relation, мультисвязь)]
|
||||||
|
|
||||||
|
**Применение:**
|
||||||
|
- RAG-ассистент использует связи между пользователями, тегами и знаниями для персонализации ответов и поиска релевантной информации.
|
||||||
|
|
||||||
|
### 5. Безопасность и контроль
|
||||||
|
- Только администратор может создавать и редактировать системные теги и знания.
|
||||||
|
- Обычные пользователи могут видеть только свои теги и связанные с ними знания.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Этот документ будет дополняться по мере реализации каждого этапа.**
|
||||||
36
backend/db/migrations/028_create_dynamic_tables.sql
Normal file
36
backend/db/migrations/028_create_dynamic_tables.sql
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
-- Миграция для динамических пользовательских таблиц (аналог Notion)
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_tables (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_columns (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
table_id INTEGER NOT NULL REFERENCES user_tables(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
type VARCHAR(50) NOT NULL, -- text, number, select, multiselect, date, etc.
|
||||||
|
options JSONB DEFAULT NULL, -- для select/multiselect
|
||||||
|
"order" INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_rows (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
table_id INTEGER NOT NULL REFERENCES user_tables(id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_cell_values (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
row_id INTEGER NOT NULL REFERENCES user_rows(id) ON DELETE CASCADE,
|
||||||
|
column_id INTEGER NOT NULL REFERENCES user_columns(id) ON DELETE CASCADE,
|
||||||
|
value TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(row_id, column_id)
|
||||||
|
);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,110 @@
|
|||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:47:23.102Z"}
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T11:09:35.925Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:47:23.103Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:09:35.927Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:47:23.103Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:09:35.927Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:47:23.103Z"}
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T11:09:51.210Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:47:23.103Z"}
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.958Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:47:23.103Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.958Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:47:23.103Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.959Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:47:23.103Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.959Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:47:23.103Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.959Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:47:23.104Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.960Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:47:23.104Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.960Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:47:23.104Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.960Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:47:23.104Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.961Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:47:23.104Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.961Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:49:29.092Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.961Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:49:29.092Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.961Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:49:29.093Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.962Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:49:29.093Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.962Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:49:29.093Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.962Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:51:57.933Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.963Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:51:57.933Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.963Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:51:57.933Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:17:27.964Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:51:57.934Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:23:00.321Z"}
|
||||||
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-05-28T13:51:57.934Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:23:00.322Z"}
|
||||||
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-05-28T13:52:49.080Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:23:00.322Z"}
|
||||||
{"body":{"language":["ru"]},"error":{"code":"22P02","detail":"Expected \":\", but found \"}\".","file":"jsonfuncs.c","length":193,"line":"646","name":"error","routine":"json_errsave_error","severity":"ERROR","where":"JSON data, line 1: {\"ru\"}\nunnamed portal parameter $1 = '...'"},"level":"error","message":"PATCH /api/users/:id error","stack":"error: invalid input syntax for type json\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/users.js:165:20","timestamp":"2025-05-28T14:00:22.853Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:23:00.322Z"}
|
||||||
{"body":{"language":["ru","en"]},"error":{"code":"22P02","detail":"Expected \":\", but found \",\".","file":"jsonfuncs.c","length":196,"line":"646","name":"error","routine":"json_errsave_error","severity":"ERROR","where":"JSON data, line 1: {\"ru\",...\nunnamed portal parameter $1 = '...'"},"level":"error","message":"PATCH /api/users/:id error","stack":"error: invalid input syntax for type json\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/users.js:165:20","timestamp":"2025-05-28T14:00:37.773Z"}
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:23:00.323Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:23:00.323Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:23:00.323Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:23:00.323Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:23:00.324Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:23:00.324Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:23:00.324Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:23:00.324Z"}
|
||||||
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T11:24:20.893Z"}
|
||||||
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T11:34:53.717Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:34:53.719Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:34:53.719Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:34:53.719Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:34:53.719Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:34:53.720Z"}
|
||||||
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T11:39:16.697Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:39:16.698Z"}
|
||||||
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T11:39:24.562Z"}
|
||||||
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T11:42:24.304Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:42:24.305Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:42:24.305Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:42:24.305Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:42:24.305Z"}
|
||||||
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T11:46:56.098Z"}
|
||||||
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T11:51:47.393Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:51:47.394Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:51:47.394Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:51:47.394Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:51:47.394Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:51:47.394Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:51:47.394Z"}
|
||||||
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T11:52:05.373Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:52:05.374Z"}
|
||||||
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T11:53:38.082Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:53:38.082Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:53:38.082Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:53:38.082Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:53:38.082Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:53:38.082Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:54:14.127Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:54:14.128Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:54:51.568Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:54:51.569Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:55:46.954Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:55:46.955Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:55:46.955Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:56:24.274Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T11:56:24.275Z"}
|
||||||
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T12:05:47.191Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:05:47.192Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:05:47.192Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:05:47.192Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:05:47.192Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:05:47.192Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:05:47.192Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:05:47.192Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:05:47.192Z"}
|
||||||
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T12:07:37.812Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:07:37.812Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:07:37.813Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:07:37.813Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:07:37.813Z"}
|
||||||
|
{"level":"error","message":"Uncaught Exception: Cannot set properties of undefined (setting 'name')","stack":"TypeError: Cannot set properties of undefined (setting 'name')\n at Connection.<anonymous> (/app/node_modules/imap/lib/Connection.js:431:22)\n at Connection._resTagged (/app/node_modules/imap/lib/Connection.js:1535:22)\n at Parser.<anonymous> (/app/node_modules/imap/lib/Connection.js:194:10)\n at Parser.emit (node:events:524:28)\n at Parser._resTagged (/app/node_modules/imap/lib/Parser.js:175:10)\n at Parser._parse (/app/node_modules/imap/lib/Parser.js:139:16)\n at Parser._tryread (/app/node_modules/imap/lib/Parser.js:82:15)\n at Parser._cbReadable (/app/node_modules/imap/lib/Parser.js:53:12)\n at TLSSocket.emit (node:events:524:28)\n at emitReadable_ (node:internal/streams/readable:834:12)","timestamp":"2025-06-01T12:07:51.222Z"}
|
||||||
|
{"level":"error","message":"Uncaught Exception: Cannot read properties of undefined (reading 'type')","stack":"TypeError: Cannot read properties of undefined (reading 'type')\n at Connection._resUntagged (/app/node_modules/imap/lib/Connection.js:1265:52)\n at Parser.<anonymous> (/app/node_modules/imap/lib/Connection.js:191:10)\n at Parser.emit (node:events:524:28)\n at Parser._resUntagged (/app/node_modules/imap/lib/Parser.js:271:10)\n at Parser._parse (/app/node_modules/imap/lib/Parser.js:137:16)\n at Parser._tryread (/app/node_modules/imap/lib/Parser.js:82:15)\n at Parser._cbReadable (/app/node_modules/imap/lib/Parser.js:53:12)\n at TLSSocket.emit (node:events:524:28)\n at emitReadable_ (node:internal/streams/readable:834:12)\n at process.processTicksAndRejections (node:internal/process/task_queues:81:21)","timestamp":"2025-06-01T12:07:51.310Z"}
|
||||||
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T12:07:51.509Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:07:51.510Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:07:51.510Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:09:28.645Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:09:28.646Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:09:28.646Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:09:28.646Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:09:46.751Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:09:46.752Z"}
|
||||||
|
{"ip":"::ffff:172.18.0.1","level":"error","message":"Error: Требуется аутентификация","method":"GET","stack":"Error: Требуется аутентификация\n at createError (/app/utils/error.js:8:17)\n at requireAdmin (/app/middleware/auth.js:97:19)\n at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)\n at next (/app/node_modules/express/lib/router/route.js:149:13)\n at Route.dispatch (/app/node_modules/express/lib/router/route.js:119:3)\n at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)\n at /app/node_modules/express/lib/router/index.js:284:15\n at Function.process_params (/app/node_modules/express/lib/router/index.js:346:12)\n at next (/app/node_modules/express/lib/router/index.js:280:10)\n at Function.handle (/app/node_modules/express/lib/router/index.js:175:3)","timestamp":"2025-06-01T12:09:57.262Z","url":"/api/settings/rpc"}
|
||||||
|
{"ip":"::ffff:172.18.0.1","level":"error","message":"Error: Требуется аутентификация","method":"GET","stack":"Error: Требуется аутентификация\n at createError (/app/utils/error.js:8:17)\n at requireAdmin (/app/middleware/auth.js:97:19)\n at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)\n at next (/app/node_modules/express/lib/router/route.js:149:13)\n at Route.dispatch (/app/node_modules/express/lib/router/route.js:119:3)\n at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)\n at /app/node_modules/express/lib/router/index.js:284:15\n at param (/app/node_modules/express/lib/router/index.js:365:14)\n at param (/app/node_modules/express/lib/router/index.js:376:14)\n at Function.process_params (/app/node_modules/express/lib/router/index.js:421:3)","timestamp":"2025-06-01T12:10:24.332Z","url":"/api/settings/ai-settings/google"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:10:37.655Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:10:37.655Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:10:37.655Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:11:33.089Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:11:33.089Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:11:33.090Z"}
|
||||||
|
{"level":"error","message":"[EmailBot] IMAP connection error: Timed out while authenticating with server","timestamp":"2025-06-01T12:13:04.750Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:13:04.751Z"}
|
||||||
|
{"level":"error","message":"IMAP connection error during check: Timed out while authenticating with server","timestamp":"2025-06-01T12:13:04.751Z"}
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ const { ERROR_CODES } = require('../utils/constants');
|
|||||||
/**
|
/**
|
||||||
* Middleware для обработки ошибок
|
* Middleware для обработки ошибок
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line no-unused-vars
|
const errorHandler = (err, req, res, next) => {
|
||||||
const errorHandler = (err, req, res, /* next */) => {
|
console.log('errorHandler called, arguments:', arguments);
|
||||||
|
console.log('typeof res:', typeof res, 'isFunction:', typeof res === 'function');
|
||||||
console.error('errorHandler: err =', err);
|
console.error('errorHandler: err =', err);
|
||||||
console.error('errorHandler: typeof err =', typeof err);
|
console.error('errorHandler: typeof err =', typeof err);
|
||||||
console.error('errorHandler: stack =', err && err.stack);
|
console.error('errorHandler: stack =', err && err.stack);
|
||||||
console.log('errorHandler called, typeof res:', typeof res, 'res:', res);
|
|
||||||
console.log('typeof res:', typeof res, 'isFunction:', typeof res === 'function');
|
|
||||||
// Логируем ошибку
|
// Логируем ошибку
|
||||||
logger.error(`Error: ${err.message}`, {
|
logger.error(`Error: ${err.message}`, {
|
||||||
stack: err.stack,
|
stack: err.stack,
|
||||||
|
|||||||
@@ -5,6 +5,5 @@
|
|||||||
"env": {
|
"env": {
|
||||||
"NODE_ENV": "development"
|
"NODE_ENV": "development"
|
||||||
},
|
},
|
||||||
"ext": "js,json,env",
|
"ext": "js,json,env"
|
||||||
"exec": "node server.js"
|
|
||||||
}
|
}
|
||||||
|
|||||||
220
backend/routes/tables.js
Normal file
220
backend/routes/tables.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const db = require('../db');
|
||||||
|
const { requireAuth } = require('../middleware/auth');
|
||||||
|
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
console.log('Tables router received:', req.method, req.originalUrl);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получить список всех таблиц (доступно всем)
|
||||||
|
router.get('/', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const result = await db.getQuery()('SELECT * FROM user_tables ORDER BY id');
|
||||||
|
res.json(result.rows);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создать новую таблицу (доступно всем)
|
||||||
|
router.post('/', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { name, description } = req.body;
|
||||||
|
const result = await db.getQuery()(
|
||||||
|
'INSERT INTO user_tables (name, description) VALUES ($1, $2) RETURNING *',
|
||||||
|
[name, description || null]
|
||||||
|
);
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получить структуру и данные таблицы (доступно всем)
|
||||||
|
router.get('/:id', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const tableId = req.params.id;
|
||||||
|
const columns = (await db.getQuery()('SELECT * FROM user_columns WHERE table_id = $1 ORDER BY "order" ASC, id ASC', [tableId])).rows;
|
||||||
|
const rows = (await db.getQuery()('SELECT * FROM user_rows WHERE table_id = $1 ORDER BY id', [tableId])).rows;
|
||||||
|
const cellValues = (await db.getQuery()('SELECT * FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId])).rows;
|
||||||
|
res.json({ columns, rows, cellValues });
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавить столбец (доступно всем)
|
||||||
|
router.post('/:id/columns', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const tableId = req.params.id;
|
||||||
|
const { name, type, options, order } = req.body;
|
||||||
|
const result = await db.getQuery()(
|
||||||
|
'INSERT INTO user_columns (table_id, name, type, options, "order") VALUES ($1, $2, $3, $4, $5) RETURNING *',
|
||||||
|
[tableId, name, type, options ? JSON.stringify(options) : null, order || 0]
|
||||||
|
);
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавить строку (доступно всем)
|
||||||
|
router.post('/:id/rows', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const tableId = req.params.id;
|
||||||
|
const result = await db.getQuery()(
|
||||||
|
'INSERT INTO user_rows (table_id) VALUES ($1) RETURNING *',
|
||||||
|
[tableId]
|
||||||
|
);
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Изменить значение ячейки (доступно всем)
|
||||||
|
router.patch('/cell/:cellId', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const cellId = req.params.cellId;
|
||||||
|
const { value } = req.body;
|
||||||
|
const result = await db.getQuery()(
|
||||||
|
'UPDATE user_cell_values SET value = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
|
||||||
|
[value, cellId]
|
||||||
|
);
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создать/обновить значение ячейки (upsert) (доступно всем)
|
||||||
|
router.post('/cell', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { row_id, column_id, value } = req.body;
|
||||||
|
const result = await db.getQuery()(
|
||||||
|
`INSERT INTO user_cell_values (row_id, column_id, value) VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (row_id, column_id) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()
|
||||||
|
RETURNING *`,
|
||||||
|
[row_id, column_id, value]
|
||||||
|
);
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удалить строку (доступно всем)
|
||||||
|
router.delete('/row/:rowId', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const rowId = req.params.rowId;
|
||||||
|
await db.getQuery()('DELETE FROM user_rows WHERE id = $1', [rowId]);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удалить столбец (доступно всем)
|
||||||
|
router.delete('/column/:columnId', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const columnId = req.params.columnId;
|
||||||
|
await db.getQuery()('DELETE FROM user_columns WHERE id = $1', [columnId]);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PATCH для обновления столбца (доступно всем)
|
||||||
|
router.patch('/column/:columnId', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const columnId = req.params.columnId;
|
||||||
|
const { name, type, options, order } = req.body;
|
||||||
|
|
||||||
|
// Построение динамического запроса
|
||||||
|
const updates = [];
|
||||||
|
const values = [];
|
||||||
|
let paramIndex = 1;
|
||||||
|
|
||||||
|
if (name !== undefined) {
|
||||||
|
updates.push(`name = $${paramIndex++}`);
|
||||||
|
values.push(name);
|
||||||
|
}
|
||||||
|
if (type !== undefined) {
|
||||||
|
updates.push(`type = $${paramIndex++}`);
|
||||||
|
values.push(type);
|
||||||
|
}
|
||||||
|
if (options !== undefined) {
|
||||||
|
updates.push(`options = $${paramIndex++}`);
|
||||||
|
values.push(options ? JSON.stringify(options) : null);
|
||||||
|
}
|
||||||
|
if (order !== undefined) {
|
||||||
|
updates.push(`"order" = $${paramIndex++}`);
|
||||||
|
values.push(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.length === 0) {
|
||||||
|
return res.status(400).json({ error: 'No fields to update' });
|
||||||
|
}
|
||||||
|
|
||||||
|
updates.push(`updated_at = NOW()`);
|
||||||
|
values.push(columnId);
|
||||||
|
|
||||||
|
const query = `UPDATE user_columns SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`;
|
||||||
|
const result = await db.getQuery()(query, values);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'Column not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PATCH: обновить название/описание таблицы (доступно всем)
|
||||||
|
router.patch('/:id', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const tableId = req.params.id;
|
||||||
|
const { name, description } = req.body;
|
||||||
|
const result = await db.getQuery()(
|
||||||
|
`UPDATE user_tables SET
|
||||||
|
name = COALESCE($1, name),
|
||||||
|
description = COALESCE($2, description),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $3 RETURNING *`,
|
||||||
|
[name, description, tableId]
|
||||||
|
);
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE: удалить таблицу и каскадно все связанные строки/столбцы/ячейки (доступно всем)
|
||||||
|
router.delete('/:id', requireAuth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const tableId = Number(req.params.id);
|
||||||
|
console.log('Backend: typeof tableId:', typeof tableId, 'value:', tableId);
|
||||||
|
// Проверяем, существует ли таблица
|
||||||
|
const checkResult = await db.getQuery()('SELECT id, name FROM user_tables WHERE id = $1', [tableId]);
|
||||||
|
console.log('Backend: Table check result:', checkResult.rows);
|
||||||
|
if (checkResult.rows.length === 0) {
|
||||||
|
console.log('Backend: Table not found');
|
||||||
|
return res.status(404).json({ error: 'Table not found' });
|
||||||
|
}
|
||||||
|
// Удаляем только основную таблицу - каскадное удаление сработает автоматически
|
||||||
|
console.log('Backend: Executing DELETE query for table_id:', tableId);
|
||||||
|
const result = await db.getQuery()('DELETE FROM user_tables WHERE id = $1', [tableId]);
|
||||||
|
console.log('Backend: Delete result - rowCount:', result.rowCount);
|
||||||
|
res.json({ success: true, deleted: result.rowCount });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Backend: Error deleting table:', err);
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -15,6 +15,8 @@ const pgSession = require('connect-pg-simple')(session);
|
|||||||
const authService = require('./services/auth-service');
|
const authService = require('./services/auth-service');
|
||||||
const logger = require('./utils/logger');
|
const logger = require('./utils/logger');
|
||||||
const EmailBotService = require('./services/emailBot.js');
|
const EmailBotService = require('./services/emailBot.js');
|
||||||
|
const tablesRouter = require('./routes/tables');
|
||||||
|
const errorHandler = require('./middleware/errorHandler');
|
||||||
|
|
||||||
const PORT = process.env.PORT || 8000;
|
const PORT = process.env.PORT || 8000;
|
||||||
|
|
||||||
@@ -94,6 +96,7 @@ app.use('/api/users', usersRouter);
|
|||||||
app.use('/api/auth', authRouter);
|
app.use('/api/auth', authRouter);
|
||||||
app.use('/api/identities', identitiesRouter);
|
app.use('/api/identities', identitiesRouter);
|
||||||
app.use('/api/chat', chatRouter);
|
app.use('/api/chat', chatRouter);
|
||||||
|
app.use('/api/tables', tablesRouter);
|
||||||
|
|
||||||
// Эндпоинт для проверки состояния сервера
|
// Эндпоинт для проверки состояния сервера
|
||||||
app.get('/api/health', (req, res) => {
|
app.get('/api/health', (req, res) => {
|
||||||
@@ -122,4 +125,6 @@ process.on('uncaughtException', (err) => {
|
|||||||
logger.error('Uncaught Exception:', err);
|
logger.error('Uncaught Exception:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use(errorHandler);
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version: '3.8'
|
# version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -60,7 +60,8 @@ services:
|
|||||||
- FRONTEND_URL=http://localhost:5173
|
- FRONTEND_URL=http://localhost:5173
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
command: sh -c "yarn run dev"
|
# command: sh -c "yarn run dev" # Временно комментируем эту строку
|
||||||
|
# command: nodemon server.js # Запускаем через nodemon
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, computed, onUnmounted } from 'vue';
|
import { ref, watch, onMounted, computed, onUnmounted } from 'vue';
|
||||||
import { RouterView } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
import { useAuth } from './composables/useAuth';
|
import { useAuth, provideAuth } from './composables/useAuth';
|
||||||
import { fetchTokenBalances } from './services/tokens';
|
import { fetchTokenBalances } from './services/tokens';
|
||||||
import eventBus from './utils/eventBus';
|
import eventBus from './utils/eventBus';
|
||||||
|
|
||||||
@@ -158,6 +158,8 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
provideAuth();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits } from 'vue';
|
import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits } from 'vue';
|
||||||
import { useAuth } from '../composables/useAuth';
|
import { useAuthContext } from '../composables/useAuth';
|
||||||
import { useAuthFlow } from '../composables/useAuthFlow';
|
import { useAuthFlow } from '../composables/useAuthFlow';
|
||||||
import { useNotifications } from '../composables/useNotifications';
|
import { useNotifications } from '../composables/useNotifications';
|
||||||
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
|
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
|
||||||
@@ -53,7 +53,7 @@ import NotificationDisplay from './NotificationDisplay.vue';
|
|||||||
// 1. ИСПОЛЬЗОВАНИЕ COMPOSABLES
|
// 1. ИСПОЛЬЗОВАНИЕ COMPOSABLES
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
const auth = useAuth();
|
const auth = useAuthContext();
|
||||||
const { notifications, showSuccessMessage, showErrorMessage } = useNotifications();
|
const { notifications, showSuccessMessage, showErrorMessage } = useNotifications();
|
||||||
|
|
||||||
// Определяем props, которые будут приходить от родительского View
|
// Определяем props, которые будут приходить от родительского View
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, onMounted, onBeforeUnmount, watch } from 'vue';
|
import { defineProps, defineEmits, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||||
import { useAuth } from '../composables/useAuth';
|
import { useAuthContext } from '../composables/useAuth';
|
||||||
import eventBus from '../utils/eventBus';
|
import eventBus from '../utils/eventBus';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -35,7 +35,7 @@ const toggleSidebar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Обработка аутентификации
|
// Обработка аутентификации
|
||||||
const auth = useAuth();
|
const auth = useAuthContext();
|
||||||
const { isAuthenticated } = auth;
|
const { isAuthenticated } = auth;
|
||||||
|
|
||||||
// Мониторинг изменений статуса аутентификации
|
// Мониторинг изменений статуса аутентификации
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ import { useRouter } from 'vue-router';
|
|||||||
import eventBus from '../utils/eventBus';
|
import eventBus from '../utils/eventBus';
|
||||||
import EmailConnect from './identity/EmailConnect.vue';
|
import EmailConnect from './identity/EmailConnect.vue';
|
||||||
import TelegramConnect from './identity/TelegramConnect.vue';
|
import TelegramConnect from './identity/TelegramConnect.vue';
|
||||||
import { useAuth } from '@/composables/useAuth';
|
import { useAuthContext } from '@/composables/useAuth';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -144,7 +144,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'wallet-auth', 'disconnect-wallet', 'telegram-auth', 'email-auth', 'cancel-email-auth']);
|
const emit = defineEmits(['update:modelValue', 'wallet-auth', 'disconnect-wallet', 'telegram-auth', 'email-auth', 'cancel-email-auth']);
|
||||||
|
|
||||||
const { deleteIdentity } = useAuth();
|
const { deleteIdentity } = useAuthContext();
|
||||||
|
|
||||||
// Обработчики событий
|
// Обработчики событий
|
||||||
const handleWalletAuth = () => {
|
const handleWalletAuth = () => {
|
||||||
|
|||||||
12
frontend/src/components/cells/CellCheckbox.vue
Normal file
12
frontend/src/components/cells/CellCheckbox.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<input type="checkbox" :checked="value === true || value === 'true'" disabled />
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
defineProps({ value: [Boolean, String, Number] });
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
input[type='checkbox'] {
|
||||||
|
pointer-events: none;
|
||||||
|
accent-color: #1976d2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
16
frontend/src/components/cells/CellDate.vue
Normal file
16
frontend/src/components/cells/CellDate.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<span>{{ formatted }}</span>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
const props = defineProps({ value: String });
|
||||||
|
const formatted = computed(() => {
|
||||||
|
if (!props.value) return '';
|
||||||
|
const d = new Date(props.value);
|
||||||
|
if (isNaN(d)) return props.value;
|
||||||
|
return d.toISOString().slice(0, 10);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
span { color: #3a3a3a; }
|
||||||
|
</style>
|
||||||
9
frontend/src/components/cells/CellNumber.vue
Normal file
9
frontend/src/components/cells/CellNumber.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<span>{{ value }}</span>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
defineProps({ value: [String, Number] });
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
span { font-variant-numeric: tabular-nums; }
|
||||||
|
</style>
|
||||||
20
frontend/src/components/cells/CellSelect.vue
Normal file
20
frontend/src/components/cells/CellSelect.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<span v-if="Array.isArray(value)">
|
||||||
|
<span v-for="(v, i) in value" :key="i" class="select-tag">{{ v }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ value }}</span>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
defineProps({ value: [String, Array], options: [Array, Object] });
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.select-tag {
|
||||||
|
display: inline-block;
|
||||||
|
background: #e0f3ff;
|
||||||
|
color: #1976d2;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
frontend/src/components/cells/CellText.vue
Normal file
9
frontend/src/components/cells/CellText.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<span>{{ value }}</span>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
defineProps({ value: String });
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
span { white-space: pre-line; }
|
||||||
|
</style>
|
||||||
@@ -38,10 +38,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import axios from '@/api/axios';
|
import axios from '@/api/axios';
|
||||||
import { useAuth } from '@/composables/useAuth';
|
import { useAuthContext } from '@/composables/useAuth';
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'success']);
|
const emit = defineEmits(['close', 'success']);
|
||||||
const { linkIdentity } = useAuth();
|
const { linkIdentity } = useAuthContext();
|
||||||
|
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
const code = ref('');
|
const code = ref('');
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { useAuth } from '@/composables/useAuth';
|
import { useAuthContext } from '@/composables/useAuth';
|
||||||
import { connectWithWallet } from '@/services/wallet';
|
import { connectWithWallet } from '@/services/wallet';
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
const { linkIdentity } = useAuth();
|
const { linkIdentity } = useAuthContext();
|
||||||
|
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
|
|||||||
111
frontend/src/components/tables/CreateTableModal.vue
Normal file
111
frontend/src/components/tables/CreateTableModal.vue
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<div class="create-table-modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Создать новую таблицу</h3>
|
||||||
|
<button class="close-btn" @click="$emit('close')">×</button>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent="createTable">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Название таблицы</label>
|
||||||
|
<input v-model="name" required maxlength="255" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Описание</label>
|
||||||
|
<textarea v-model="description" maxlength="500"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-success" type="submit" :disabled="isLoading">Создать</button>
|
||||||
|
<button class="btn btn-secondary" type="button" @click="$emit('close')">Отмена</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import tablesService from '../../services/tablesService';
|
||||||
|
const emit = defineEmits(['close', 'table-created']);
|
||||||
|
const name = ref('');
|
||||||
|
const description = ref('');
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const error = ref('');
|
||||||
|
|
||||||
|
async function createTable() {
|
||||||
|
if (!name.value.trim()) return;
|
||||||
|
isLoading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
await tablesService.createTable({ name: name.value, description: description.value });
|
||||||
|
emit('table-created');
|
||||||
|
emit('close');
|
||||||
|
} catch (e) {
|
||||||
|
error.value = 'Ошибка создания таблицы';
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.create-table-modal {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 16px rgba(0,0,0,0.13);
|
||||||
|
padding: 28px 22px 18px 22px;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 40px auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
input, textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 7px 18px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: #bbb;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: #dc3545;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
276
frontend/src/components/tables/DynamicTableEditor.vue
Normal file
276
frontend/src/components/tables/DynamicTableEditor.vue
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dynamic-table-editor">
|
||||||
|
<div class="editor-header">
|
||||||
|
<input v-model="tableName" @blur="saveTableName" class="table-title-input" />
|
||||||
|
<textarea v-model="tableDesc" @blur="saveTableDesc" class="table-desc-input" />
|
||||||
|
<button class="close-btn" @click="$emit('close')">×</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="isLoading" class="loading">Загрузка...</div>
|
||||||
|
<div v-else>
|
||||||
|
<TableColumnsDraggable
|
||||||
|
:columns="columns"
|
||||||
|
@update-column="updateColumn"
|
||||||
|
@delete-column="deleteColumn"
|
||||||
|
@edit-options="openOptionsEditor"
|
||||||
|
@columns-reordered="reorderColumns"
|
||||||
|
/>
|
||||||
|
<SelectOptionsEditor
|
||||||
|
v-if="showOptionsEditor"
|
||||||
|
:options="editingOptions"
|
||||||
|
@update:options="saveOptions"
|
||||||
|
/>
|
||||||
|
<div class="table-controls">
|
||||||
|
<button class="btn btn-success" @click="addColumn">Добавить столбец</button>
|
||||||
|
<button class="btn btn-success" @click="addRow">Добавить строку</button>
|
||||||
|
</div>
|
||||||
|
<table class="dynamic-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="col in columns" :key="col.id">{{ col.name }}</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="row in rows" :key="row.id">
|
||||||
|
<td v-for="col in columns" :key="col.id">
|
||||||
|
<template v-if="col.type === 'select'">
|
||||||
|
<select v-model="cellEdits[`${row.id}_${col.id}`]" @change="saveCell(row.id, col.id)">
|
||||||
|
<option v-for="opt in col.options || []" :key="opt" :value="opt">{{ opt }}</option>
|
||||||
|
</select>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<input :value="cellValue(row.id, col.id)" @input="onCellInput(row.id, col.id, $event.target.value)" @blur="saveCell(row.id, col.id)" />
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-danger btn-sm" @click="deleteRow(row.id)">Удалить</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div v-if="!rows.length || !columns.length" class="empty-table">Нет данных. Добавьте столбцы и строки.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch } from 'vue';
|
||||||
|
import tablesService from '../../services/tablesService';
|
||||||
|
import TableColumnsDraggable from './TableColumnsDraggable.vue';
|
||||||
|
import SelectOptionsEditor from './SelectOptionsEditor.vue';
|
||||||
|
|
||||||
|
const props = defineProps({ tableId: { type: Number, required: true } });
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
const isLoading = ref(true);
|
||||||
|
const columns = ref([]);
|
||||||
|
const rows = ref([]);
|
||||||
|
const cellValues = ref([]);
|
||||||
|
const cellEdits = ref({});
|
||||||
|
const tableName = ref('');
|
||||||
|
const tableDesc = ref('');
|
||||||
|
const showOptionsEditor = ref(false);
|
||||||
|
const editingCol = ref(null);
|
||||||
|
const editingOptions = ref([]);
|
||||||
|
|
||||||
|
function loadTable() {
|
||||||
|
isLoading.value = true;
|
||||||
|
tablesService.getTable(props.tableId)
|
||||||
|
.then(res => {
|
||||||
|
columns.value = res.columns;
|
||||||
|
rows.value = res.rows;
|
||||||
|
cellValues.value = res.cellValues;
|
||||||
|
cellEdits.value = {};
|
||||||
|
tableName.value = res.columns.length ? res.columns[0].table_name : '';
|
||||||
|
tableDesc.value = res.columns.length ? res.columns[0].table_description : '';
|
||||||
|
})
|
||||||
|
.finally(() => { isLoading.value = false; });
|
||||||
|
}
|
||||||
|
function addColumn() {
|
||||||
|
const name = prompt('Название столбца:');
|
||||||
|
if (!name) return;
|
||||||
|
tablesService.addColumn(props.tableId, { name, type: 'text' }).then(loadTable);
|
||||||
|
}
|
||||||
|
function addRow() {
|
||||||
|
tablesService.addRow(props.tableId).then(loadTable);
|
||||||
|
}
|
||||||
|
function deleteColumn(colId) {
|
||||||
|
if (!confirm('Удалить столбец?')) return;
|
||||||
|
tablesService.deleteColumn(colId).then(loadTable);
|
||||||
|
}
|
||||||
|
function deleteRow(rowId) {
|
||||||
|
if (!confirm('Удалить строку?')) return;
|
||||||
|
tablesService.deleteRow(rowId).then(loadTable);
|
||||||
|
}
|
||||||
|
function cellValue(rowId, colId) {
|
||||||
|
const key = `${rowId}_${colId}`;
|
||||||
|
if (cellEdits.value[key] !== undefined) return cellEdits.value[key];
|
||||||
|
const found = cellValues.value.find(c => c.row_id === rowId && c.column_id === colId);
|
||||||
|
return found ? found.value : '';
|
||||||
|
}
|
||||||
|
function saveCell(rowId, colId) {
|
||||||
|
const key = `${rowId}_${colId}`;
|
||||||
|
const value = cellEdits.value[key];
|
||||||
|
tablesService.saveCell({ row_id: rowId, column_id: colId, value }).then(loadTable);
|
||||||
|
}
|
||||||
|
function updateColumn(col) {
|
||||||
|
try {
|
||||||
|
// Убеждаемся, что options - это массив или null
|
||||||
|
let options = col.options;
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
try {
|
||||||
|
options = JSON.parse(options);
|
||||||
|
} catch {
|
||||||
|
options = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tablesService.updateColumn(col.id, {
|
||||||
|
name: col.name,
|
||||||
|
type: col.type,
|
||||||
|
options: options,
|
||||||
|
order: col.order
|
||||||
|
}).then(loadTable).catch(err => {
|
||||||
|
console.error('Ошибка обновления столбца:', err);
|
||||||
|
alert('Ошибка обновления столбца');
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка updateColumn:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function reorderColumns(newColumns) {
|
||||||
|
// Сохраняем новый порядок столбцов последовательно
|
||||||
|
const updatePromises = newColumns.map((col, idx) =>
|
||||||
|
tablesService.updateColumn(col.id, { order: idx })
|
||||||
|
);
|
||||||
|
|
||||||
|
Promise.all(updatePromises)
|
||||||
|
.then(() => loadTable())
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Ошибка переупорядочивания столбцов:', err);
|
||||||
|
alert('Ошибка переупорядочивания столбцов');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function onCellInput(rowId, colId, value) {
|
||||||
|
const key = `${rowId}_${colId}`;
|
||||||
|
cellEdits.value[key] = value;
|
||||||
|
}
|
||||||
|
function openOptionsEditor(col) {
|
||||||
|
editingCol.value = col;
|
||||||
|
editingOptions.value = Array.isArray(col.options) ? [...col.options] : [];
|
||||||
|
showOptionsEditor.value = true;
|
||||||
|
}
|
||||||
|
function saveOptions(newOptions) {
|
||||||
|
if (!editingCol.value) return;
|
||||||
|
tablesService.updateColumn(editingCol.value.id, { options: newOptions }).then(() => {
|
||||||
|
showOptionsEditor.value = false;
|
||||||
|
loadTable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function saveTableName() {
|
||||||
|
tablesService.updateTable(props.tableId, { name: tableName.value });
|
||||||
|
}
|
||||||
|
function saveTableDesc() {
|
||||||
|
tablesService.updateTable(props.tableId, { description: tableDesc.value });
|
||||||
|
}
|
||||||
|
watch([columns, rows], () => {
|
||||||
|
cellEdits.value = {};
|
||||||
|
});
|
||||||
|
onMounted(loadTable);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dynamic-table-editor {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 16px rgba(0,0,0,0.13);
|
||||||
|
padding: 28px 22px 18px 22px;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 40px auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.editor-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.table-title-input {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: bold;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
flex: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.table-desc-input {
|
||||||
|
font-size: 0.9em;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
outline: none;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
resize: none;
|
||||||
|
height: 60px;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.table-controls {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.dynamic-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.dynamic-table th, .dynamic-table td {
|
||||||
|
border: 1px solid #eee;
|
||||||
|
padding: 6px 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.dynamic-table th {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
color: #888;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
.empty-table {
|
||||||
|
color: #aaa;
|
||||||
|
text-align: center;
|
||||||
|
margin: 24px 0;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
100
frontend/src/components/tables/DynamicTablesModal.vue
Normal file
100
frontend/src/components/tables/DynamicTablesModal.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dynamic-tables-modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Пользовательские таблицы</h2>
|
||||||
|
<button class="close-btn" @click="$emit('close')">×</button>
|
||||||
|
</div>
|
||||||
|
<UserTablesList @open-table="openTable" @table-deleted="onTableDeleted" />
|
||||||
|
<DynamicTableEditor v-if="selectedTable" :table-id="selectedTable.id" @close="closeEditor" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import UserTablesList from './UserTablesList.vue';
|
||||||
|
import DynamicTableEditor from './DynamicTableEditor.vue';
|
||||||
|
|
||||||
|
const selectedTable = ref(null);
|
||||||
|
function openTable(table) {
|
||||||
|
selectedTable.value = table;
|
||||||
|
}
|
||||||
|
function closeEditor() {
|
||||||
|
selectedTable.value = null;
|
||||||
|
}
|
||||||
|
function onTableDeleted(deletedTableId) {
|
||||||
|
if (selectedTable.value && selectedTable.value.id === deletedTableId) {
|
||||||
|
selectedTable.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dynamic-tables-modal {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||||
|
padding: 32px 24px 24px 24px;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 40px auto;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bbb;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.tables-list-block {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.tables-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.tables-list li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
color: #888;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
.user-table-block {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 18px 12px;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
.user-table-full {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.user-table-full th, .user-table-full td {
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
padding: 6px 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.user-table-full th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
68
frontend/src/components/tables/SelectOptionsEditor.vue
Normal file
68
frontend/src/components/tables/SelectOptionsEditor.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div class="select-options-editor">
|
||||||
|
<h4>Опции для select</h4>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(opt, idx) in localOptions" :key="idx">
|
||||||
|
<input v-model="localOptions[idx]" @blur="emitOptions" class="option-input" />
|
||||||
|
<button class="btn btn-danger btn-xs" @click="removeOption(idx)">×</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button class="btn btn-success btn-xs" @click="addOption">Добавить опцию</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
const props = defineProps({
|
||||||
|
options: { type: Array, default: () => [] }
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['update:options']);
|
||||||
|
const localOptions = ref([...props.options]);
|
||||||
|
|
||||||
|
watch(() => props.options, (val) => {
|
||||||
|
localOptions.value = [...val];
|
||||||
|
});
|
||||||
|
function addOption() {
|
||||||
|
localOptions.value.push('');
|
||||||
|
emitOptions();
|
||||||
|
}
|
||||||
|
function removeOption(idx) {
|
||||||
|
localOptions.value.splice(idx, 1);
|
||||||
|
emitOptions();
|
||||||
|
}
|
||||||
|
function emitOptions() {
|
||||||
|
emit('update:options', localOptions.value.filter(opt => opt.trim() !== ''));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.select-options-editor {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.option-input {
|
||||||
|
flex: 1 1 80px;
|
||||||
|
border: 1px solid #b0b0b0;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
.btn-xs {
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
104
frontend/src/components/tables/TableColumnsDraggable.vue
Normal file
104
frontend/src/components/tables/TableColumnsDraggable.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div class="columns-container">
|
||||||
|
<div v-for="(col, index) in localColumns" :key="col.id" class="column-header">
|
||||||
|
<input v-model="col.name" @blur="updateColumn(col)" class="col-name-input" :placeholder="'Название'" />
|
||||||
|
<select v-model="col.type" @change="updateColumn(col)" class="col-type-select">
|
||||||
|
<option value="text">Текст</option>
|
||||||
|
<option value="select">Список</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-danger btn-sm" @click="$emit('delete-column', col.id)">×</button>
|
||||||
|
<button v-if="col.type==='select'" class="btn btn-secondary btn-xs" @click="$emit('edit-options', col)">Опции</button>
|
||||||
|
<div class="reorder-buttons">
|
||||||
|
<button v-if="index > 0" class="btn btn-light btn-xs" @click="moveColumn(index, -1)">←</button>
|
||||||
|
<button v-if="index < localColumns.length - 1" class="btn btn-light btn-xs" @click="moveColumn(index, 1)">→</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
const props = defineProps({
|
||||||
|
columns: { type: Array, required: true }
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['update:columns', 'update-column', 'delete-column', 'edit-options', 'columns-reordered']);
|
||||||
|
const localColumns = ref([...props.columns]);
|
||||||
|
|
||||||
|
watch(() => props.columns, (val) => {
|
||||||
|
localColumns.value = [...val];
|
||||||
|
});
|
||||||
|
function updateColumn(col) {
|
||||||
|
emit('update-column', col);
|
||||||
|
}
|
||||||
|
function moveColumn(index, direction) {
|
||||||
|
const newIndex = index + direction;
|
||||||
|
if (newIndex >= 0 && newIndex < localColumns.value.length) {
|
||||||
|
const columns = [...localColumns.value];
|
||||||
|
[columns[index], columns[newIndex]] = [columns[newIndex], columns[index]];
|
||||||
|
localColumns.value = columns;
|
||||||
|
emit('columns-reordered', localColumns.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.columns-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.column-header {
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 180px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.col-name-input {
|
||||||
|
flex: 1 1 80px;
|
||||||
|
border: 1px solid #b0b0b0;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
.col-type-select {
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
}
|
||||||
|
.btn-light {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.btn-xs {
|
||||||
|
font-size: 0.7em;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 0.75em;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
.reorder-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
167
frontend/src/components/tables/UserTablesList.vue
Normal file
167
frontend/src/components/tables/UserTablesList.vue
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-tables-list">
|
||||||
|
<div class="header-block">
|
||||||
|
<h2>Пользовательские таблицы</h2>
|
||||||
|
<button class="btn btn-success" @click="showCreateTable = true">Создать таблицу</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="isLoading" class="loading">Загрузка...</div>
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="tables.length === 0" class="empty-block">Нет таблиц. Создайте первую!</div>
|
||||||
|
<div v-else class="tables-cards">
|
||||||
|
<div v-for="table in tables" :key="table.id" class="table-card">
|
||||||
|
<div class="table-card-header">
|
||||||
|
<input v-if="editingTableId === table.id" v-model="editName" @blur="saveName(table)" @keyup.enter="saveName(table)" class="table-name-input" />
|
||||||
|
<h3 v-else @dblclick="startEditName(table)">{{ table.name }}</h3>
|
||||||
|
<div class="table-card-actions">
|
||||||
|
<button class="btn btn-info btn-sm" @click="$emit('open-table', table)">Открыть</button>
|
||||||
|
<button class="btn btn-warning btn-sm" @click="startEditName(table)">Переименовать</button>
|
||||||
|
<button class="btn btn-danger btn-sm" @click="deleteTable(table)">Удалить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-card-desc">
|
||||||
|
<textarea v-if="editingDescId === table.id" v-model="editDesc" @blur="saveDesc(table)" @keyup.enter="saveDesc(table)" class="table-desc-input" />
|
||||||
|
<p v-else @dblclick="startEditDesc(table)">{{ table.description || 'Без описания' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CreateTableModal v-if="showCreateTable" @close="showCreateTable = false" @table-created="onTableCreated" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import tablesService from '../../services/tablesService';
|
||||||
|
import CreateTableModal from './CreateTableModal.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['open-table', 'table-deleted']);
|
||||||
|
const tables = ref([]);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const showCreateTable = ref(false);
|
||||||
|
const editingTableId = ref(null);
|
||||||
|
const editName = ref('');
|
||||||
|
const editingDescId = ref(null);
|
||||||
|
const editDesc = ref('');
|
||||||
|
|
||||||
|
function loadTables() {
|
||||||
|
isLoading.value = true;
|
||||||
|
tablesService.getTables()
|
||||||
|
.then(res => {
|
||||||
|
tables.value = [...res]; // Создаем новый массив для принудительного обновления
|
||||||
|
})
|
||||||
|
.finally(() => { isLoading.value = false; });
|
||||||
|
}
|
||||||
|
function onTableCreated() {
|
||||||
|
showCreateTable.value = false;
|
||||||
|
loadTables();
|
||||||
|
}
|
||||||
|
function startEditName(table) {
|
||||||
|
editingTableId.value = table.id;
|
||||||
|
editName.value = table.name;
|
||||||
|
}
|
||||||
|
function saveName(table) {
|
||||||
|
if (editName.value && editName.value !== table.name) {
|
||||||
|
tablesService.updateTable(table.id, { name: editName.value })
|
||||||
|
.then(loadTables);
|
||||||
|
}
|
||||||
|
editingTableId.value = null;
|
||||||
|
}
|
||||||
|
function startEditDesc(table) {
|
||||||
|
editingDescId.value = table.id;
|
||||||
|
editDesc.value = table.description || '';
|
||||||
|
}
|
||||||
|
function saveDesc(table) {
|
||||||
|
tablesService.updateTable(table.id, { description: editDesc.value })
|
||||||
|
.then(loadTables);
|
||||||
|
editingDescId.value = null;
|
||||||
|
}
|
||||||
|
function deleteTable(table) {
|
||||||
|
console.log('deleteTable called with:', table);
|
||||||
|
|
||||||
|
if (!confirm(`Удалить таблицу "${table.name}"?`)) {
|
||||||
|
console.log('User cancelled deletion');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('User confirmed deletion, proceeding...');
|
||||||
|
|
||||||
|
// Немедленно удаляем из локального списка для быстрой реакции UI
|
||||||
|
tables.value = tables.value.filter(t => t.id !== table.id);
|
||||||
|
console.log('Removed from local list, making API call...');
|
||||||
|
|
||||||
|
tablesService.deleteTable(table.id)
|
||||||
|
.then((result) => {
|
||||||
|
console.log('Таблица удалена:', result);
|
||||||
|
// Уведомляем родительский компонент об удалении
|
||||||
|
emit('table-deleted', table.id);
|
||||||
|
// Принудительно обновляем список с сервера для синхронизации
|
||||||
|
loadTables();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Ошибка удаления таблицы:', error);
|
||||||
|
alert('Ошибка при удалении таблицы');
|
||||||
|
// При ошибке восстанавливаем список с сервера
|
||||||
|
loadTables();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onMounted(loadTables);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-tables-list {
|
||||||
|
padding: 18px 8px;
|
||||||
|
}
|
||||||
|
.header-block {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.tables-cards {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
.table-card {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
min-width: 260px;
|
||||||
|
max-width: 340px;
|
||||||
|
flex: 1 1 260px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.table-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.table-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.table-card-desc {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.table-name-input, .table-desc-input {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1.1em;
|
||||||
|
border: 1px solid #b0b0b0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
.empty-block {
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
margin: 32px 0;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
color: #888;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,474 +1,492 @@
|
|||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
import { ref, onMounted, onUnmounted, provide, inject } from 'vue';
|
||||||
import axios from '../api/axios';
|
import axios from '../api/axios';
|
||||||
|
|
||||||
export function useAuth() {
|
// === SINGLETON STATE ===
|
||||||
const isAuthenticated = ref(false);
|
const isAuthenticated = ref(false);
|
||||||
const authType = ref(null);
|
const authType = ref(null);
|
||||||
const userId = ref(null);
|
const userId = ref(null);
|
||||||
const address = ref(null);
|
const address = ref(null);
|
||||||
const telegramId = ref(null);
|
const telegramId = ref(null);
|
||||||
const isAdmin = ref(false);
|
const isAdmin = ref(false);
|
||||||
const email = ref(null);
|
const email = ref(null);
|
||||||
const processedGuestIds = ref([]);
|
const processedGuestIds = ref([]);
|
||||||
const identities = ref([]);
|
const identities = ref([]);
|
||||||
const tokenBalances = ref([]);
|
const tokenBalances = ref([]);
|
||||||
|
|
||||||
// Функция для обновления списка идентификаторов
|
// Функция для обновления списка идентификаторов
|
||||||
const updateIdentities = async () => {
|
const updateIdentities = async () => {
|
||||||
if (!isAuthenticated.value || !userId.value) return;
|
if (!isAuthenticated.value || !userId.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/auth/identities');
|
const response = await axios.get('/api/auth/identities');
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
// Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные
|
// Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные
|
||||||
const filteredIdentities = response.data.identities
|
const filteredIdentities = response.data.identities
|
||||||
.filter((identity) => identity.provider !== 'guest')
|
.filter((identity) => identity.provider !== 'guest')
|
||||||
.reduce((acc, identity) => {
|
.reduce((acc, identity) => {
|
||||||
// Для каждого типа провайдера оставляем только один идентификатор
|
// Для каждого типа провайдера оставляем только один идентификатор
|
||||||
const existingIdentity = acc.find((i) => i.provider === identity.provider);
|
const existingIdentity = acc.find((i) => i.provider === identity.provider);
|
||||||
if (!existingIdentity) {
|
if (!existingIdentity) {
|
||||||
acc.push(identity);
|
acc.push(identity);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Сравниваем новый отфильтрованный список с текущим значением
|
// Сравниваем новый отфильтрованный список с текущим значением
|
||||||
const currentProviders = identities.value.map(id => id.provider).sort();
|
const currentProviders = identities.value.map(id => id.provider).sort();
|
||||||
const newProviders = filteredIdentities.map(id => id.provider).sort();
|
const newProviders = filteredIdentities.map(id => id.provider).sort();
|
||||||
|
|
||||||
const identitiesChanged = JSON.stringify(currentProviders) !== JSON.stringify(newProviders);
|
const identitiesChanged = JSON.stringify(currentProviders) !== JSON.stringify(newProviders);
|
||||||
|
|
||||||
// Обновляем реактивное значение
|
// Обновляем реактивное значение
|
||||||
identities.value = filteredIdentities;
|
identities.value = filteredIdentities;
|
||||||
console.log('User identities updated:', identities.value);
|
console.log('User identities updated:', identities.value);
|
||||||
|
|
||||||
// Если список идентификаторов изменился, принудительно проверяем аутентификацию,
|
// Если список идентификаторов изменился, принудительно проверяем аутентификацию,
|
||||||
// чтобы обновить authType и другие связанные данные (например, telegramId)
|
// чтобы обновить authType и другие связанные данные (например, telegramId)
|
||||||
if (identitiesChanged) {
|
if (identitiesChanged) {
|
||||||
console.log('Identities changed, forcing auth check.');
|
console.log('Identities changed, forcing auth check.');
|
||||||
await checkAuth(); // Вызываем checkAuth для обновления полного состояния
|
await checkAuth(); // Вызываем checkAuth для обновления полного состояния
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching user identities:', error);
|
|
||||||
}
|
}
|
||||||
};
|
} catch (error) {
|
||||||
|
console.error('Error fetching user identities:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Периодическое обновление идентификаторов
|
// Периодическое обновление идентификаторов
|
||||||
let identitiesInterval;
|
let identitiesInterval;
|
||||||
|
|
||||||
const startIdentitiesPolling = () => {
|
const startIdentitiesPolling = () => {
|
||||||
if (identitiesInterval) return;
|
if (identitiesInterval) return;
|
||||||
identitiesInterval = setInterval(updateIdentities, 30000); // Обновляем каждые 30 секунд
|
identitiesInterval = setInterval(updateIdentities, 30000); // Обновляем каждые 30 секунд
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopIdentitiesPolling = () => {
|
const stopIdentitiesPolling = () => {
|
||||||
if (identitiesInterval) {
|
if (identitiesInterval) {
|
||||||
clearInterval(identitiesInterval);
|
clearInterval(identitiesInterval);
|
||||||
identitiesInterval = null;
|
identitiesInterval = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkTokenBalances = async (address) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/auth/check-tokens/${address}`);
|
||||||
|
if (response.data.success) {
|
||||||
|
tokenBalances.value = response.data.balances;
|
||||||
|
return response.data.balances;
|
||||||
}
|
}
|
||||||
};
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking token balances:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const checkTokenBalances = async (address) => {
|
const updateAuth = async ({
|
||||||
try {
|
authenticated,
|
||||||
const response = await axios.get(`/api/auth/check-tokens/${address}`);
|
authType: newAuthType,
|
||||||
if (response.data.success) {
|
userId: newUserId,
|
||||||
tokenBalances.value = response.data.balances;
|
address: newAddress,
|
||||||
return response.data.balances;
|
telegramId: newTelegramId,
|
||||||
}
|
isAdmin: newIsAdmin,
|
||||||
return null;
|
email: newEmail,
|
||||||
} catch (error) {
|
}) => {
|
||||||
console.error('Error checking token balances:', error);
|
const wasAuthenticated = isAuthenticated.value;
|
||||||
return null;
|
const previousUserId = userId.value;
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateAuth = async ({
|
console.log('updateAuth called with:', {
|
||||||
authenticated,
|
authenticated,
|
||||||
authType: newAuthType,
|
newAuthType,
|
||||||
userId: newUserId,
|
newUserId,
|
||||||
address: newAddress,
|
newAddress,
|
||||||
telegramId: newTelegramId,
|
newTelegramId,
|
||||||
isAdmin: newIsAdmin,
|
newIsAdmin,
|
||||||
email: newEmail,
|
newEmail,
|
||||||
}) => {
|
});
|
||||||
const wasAuthenticated = isAuthenticated.value;
|
|
||||||
const previousUserId = userId.value;
|
|
||||||
|
|
||||||
console.log('updateAuth called with:', {
|
// Убедимся, что переменные являются реактивными
|
||||||
|
isAuthenticated.value = authenticated === true;
|
||||||
|
authType.value = newAuthType || null;
|
||||||
|
userId.value = newUserId || null;
|
||||||
|
address.value = newAddress || null;
|
||||||
|
telegramId.value = newTelegramId || null;
|
||||||
|
isAdmin.value = newIsAdmin === true;
|
||||||
|
email.value = newEmail || null;
|
||||||
|
|
||||||
|
// Кэшируем данные аутентификации
|
||||||
|
localStorage.setItem(
|
||||||
|
'authData',
|
||||||
|
JSON.stringify({
|
||||||
authenticated,
|
authenticated,
|
||||||
newAuthType,
|
authType: newAuthType,
|
||||||
newUserId,
|
userId: newUserId,
|
||||||
newAddress,
|
address: newAddress,
|
||||||
newTelegramId,
|
telegramId: newTelegramId,
|
||||||
newIsAdmin,
|
isAdmin: newIsAdmin,
|
||||||
newEmail,
|
email: newEmail,
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Убедимся, что переменные являются реактивными
|
// Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса
|
||||||
isAuthenticated.value = authenticated === true;
|
if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) {
|
||||||
authType.value = newAuthType || null;
|
await checkTokenBalances(newAddress);
|
||||||
userId.value = newUserId || null;
|
}
|
||||||
address.value = newAddress || null;
|
|
||||||
telegramId.value = newTelegramId || null;
|
|
||||||
isAdmin.value = newIsAdmin === true;
|
|
||||||
email.value = newEmail || null;
|
|
||||||
|
|
||||||
// Кэшируем данные аутентификации
|
// Обновляем идентификаторы при любом изменении аутентификации
|
||||||
localStorage.setItem(
|
if (authenticated) {
|
||||||
'authData',
|
await updateIdentities();
|
||||||
JSON.stringify({
|
startIdentitiesPolling();
|
||||||
authenticated,
|
} else {
|
||||||
authType: newAuthType,
|
stopIdentitiesPolling();
|
||||||
userId: newUserId,
|
identities.value = [];
|
||||||
address: newAddress,
|
}
|
||||||
telegramId: newTelegramId,
|
|
||||||
isAdmin: newIsAdmin,
|
|
||||||
email: newEmail,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса
|
console.log('Auth updated:', {
|
||||||
if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) {
|
authenticated: isAuthenticated.value,
|
||||||
await checkTokenBalances(newAddress);
|
userId: userId.value,
|
||||||
}
|
address: address.value,
|
||||||
|
telegramId: telegramId.value,
|
||||||
|
email: email.value,
|
||||||
|
isAdmin: isAdmin.value,
|
||||||
|
});
|
||||||
|
|
||||||
// Обновляем идентификаторы при любом изменении аутентификации
|
// Если пользователь только что аутентифицировался или сменил аккаунт,
|
||||||
if (authenticated) {
|
// пробуем связать сообщения
|
||||||
await updateIdentities();
|
if (authenticated && (!wasAuthenticated || (previousUserId && previousUserId !== newUserId))) {
|
||||||
startIdentitiesPolling();
|
console.log('Auth change detected, linking messages');
|
||||||
} else {
|
linkMessages();
|
||||||
stopIdentitiesPolling();
|
}
|
||||||
identities.value = [];
|
};
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Auth updated:', {
|
// Функция для связывания сообщений после успешной авторизации
|
||||||
authenticated: isAuthenticated.value,
|
const linkMessages = async () => {
|
||||||
userId: userId.value,
|
try {
|
||||||
address: address.value,
|
if (isAuthenticated.value) {
|
||||||
telegramId: telegramId.value,
|
console.log('Linking messages after authentication');
|
||||||
email: email.value,
|
|
||||||
isAdmin: isAdmin.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Если пользователь только что аутентифицировался или сменил аккаунт,
|
// Проверка, есть ли гостевой ID для обработки
|
||||||
// пробуем связать сообщения
|
const localGuestId = localStorage.getItem('guestId');
|
||||||
if (authenticated && (!wasAuthenticated || (previousUserId && previousUserId !== newUserId))) {
|
|
||||||
console.log('Auth change detected, linking messages');
|
|
||||||
linkMessages();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для связывания сообщений после успешной авторизации
|
// Если гостевого ID нет или он уже был обработан, пропускаем запрос
|
||||||
const linkMessages = async () => {
|
if (!localGuestId || processedGuestIds.value.includes(localGuestId)) {
|
||||||
try {
|
console.log('No new guest IDs to process or already processed');
|
||||||
if (isAuthenticated.value) {
|
return {
|
||||||
console.log('Linking messages after authentication');
|
success: true,
|
||||||
|
message: 'No new guest IDs to process',
|
||||||
|
processedIds: processedGuestIds.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Проверка, есть ли гостевой ID для обработки
|
// Создаем объект с идентификаторами для передачи на сервер
|
||||||
const localGuestId = localStorage.getItem('guestId');
|
const identifiersData = {
|
||||||
|
userId: userId.value,
|
||||||
|
guestId: localGuestId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Добавляем все доступные идентификаторы
|
||||||
|
if (address.value) identifiersData.address = address.value;
|
||||||
|
if (email.value) identifiersData.email = email.value;
|
||||||
|
if (telegramId.value) identifiersData.telegramId = telegramId.value;
|
||||||
|
|
||||||
|
console.log('Sending link-guest-messages request with data:', identifiersData);
|
||||||
|
|
||||||
|
/* Удаляем ненужный вызов
|
||||||
|
try {
|
||||||
|
// Отправляем запрос на связывание сообщений
|
||||||
|
const response = await axios.post('/api/auth/link-guest-messages', identifiersData);
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
console.log('Messages linked successfully:', response.data);
|
||||||
|
|
||||||
|
// Обновляем список обработанных guestIds из ответа сервера
|
||||||
|
if (response.data.processedIds && Array.isArray(response.data.processedIds)) {
|
||||||
|
processedGuestIds.value = [...response.data.processedIds];
|
||||||
|
console.log('Updated processed guest IDs from server:', processedGuestIds.value);
|
||||||
|
}
|
||||||
|
// В качестве запасного варианта также обрабатываем старый формат ответа
|
||||||
|
else if (response.data.results && Array.isArray(response.data.results)) {
|
||||||
|
const newProcessedIds = response.data.results
|
||||||
|
.filter((result) => result.guestId)
|
||||||
|
.map((result) => result.guestId);
|
||||||
|
|
||||||
|
if (newProcessedIds.length > 0) {
|
||||||
|
processedGuestIds.value = [
|
||||||
|
...new Set([...processedGuestIds.value, ...newProcessedIds]),
|
||||||
|
];
|
||||||
|
console.log('Updated processed guest IDs from results:', processedGuestIds.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем гостевые сообщения из localStorage после успешного связывания
|
||||||
|
localStorage.removeItem('guestMessages');
|
||||||
|
localStorage.removeItem('guestId');
|
||||||
|
|
||||||
// Если гостевого ID нет или он уже был обработан, пропускаем запрос
|
|
||||||
if (!localGuestId || processedGuestIds.value.includes(localGuestId)) {
|
|
||||||
console.log('No new guest IDs to process or already processed');
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: 'No new guest IDs to process',
|
|
||||||
processedIds: processedGuestIds.value,
|
processedIds: processedGuestIds.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
// Создаем объект с идентификаторами для передачи на сервер
|
console.error('Error linking messages:', error);
|
||||||
const identifiersData = {
|
return {
|
||||||
userId: userId.value,
|
success: false,
|
||||||
guestId: localGuestId,
|
error: error.message,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// Предполагаем, что бэкенд автоматически связывает сообщения
|
||||||
|
// Очищаем данные гостя локально
|
||||||
|
console.log('Assuming backend handles message linking. Clearing local guest data.');
|
||||||
|
localStorage.removeItem('guestMessages');
|
||||||
|
localStorage.removeItem('guestId');
|
||||||
|
// Добавляем текущий guestId в обработанные, чтобы не пытаться отправить его снова
|
||||||
|
if(localGuestId) {
|
||||||
|
updateProcessedGuestIds([localGuestId]);
|
||||||
|
}
|
||||||
|
return { success: true, message: 'Local guest data cleared.' };
|
||||||
|
|
||||||
// Добавляем все доступные идентификаторы
|
}
|
||||||
if (address.value) identifiersData.address = address.value;
|
|
||||||
if (email.value) identifiersData.email = email.value;
|
|
||||||
if (telegramId.value) identifiersData.telegramId = telegramId.value;
|
|
||||||
|
|
||||||
console.log('Sending link-guest-messages request with data:', identifiersData);
|
return { success: false, message: 'Not authenticated' };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in linkMessages:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/* Удаляем ненужный вызов
|
const checkAuth = async () => {
|
||||||
try {
|
try {
|
||||||
// Отправляем запрос на связывание сообщений
|
const response = await axios.get('/api/auth/check');
|
||||||
const response = await axios.post('/api/auth/link-guest-messages', identifiersData);
|
console.log('Auth check response:', response.data);
|
||||||
|
|
||||||
if (response.data.success) {
|
const wasAuthenticated = isAuthenticated.value;
|
||||||
console.log('Messages linked successfully:', response.data);
|
const previousUserId = userId.value;
|
||||||
|
const previousAuthType = authType.value;
|
||||||
|
|
||||||
// Обновляем список обработанных guestIds из ответа сервера
|
// Обновляем данные авторизации через updateAuth вместо прямого изменения
|
||||||
if (response.data.processedIds && Array.isArray(response.data.processedIds)) {
|
await updateAuth({
|
||||||
processedGuestIds.value = [...response.data.processedIds];
|
authenticated: response.data.authenticated,
|
||||||
console.log('Updated processed guest IDs from server:', processedGuestIds.value);
|
authType: response.data.authType,
|
||||||
}
|
userId: response.data.userId,
|
||||||
// В качестве запасного варианта также обрабатываем старый формат ответа
|
address: response.data.address,
|
||||||
else if (response.data.results && Array.isArray(response.data.results)) {
|
telegramId: response.data.telegramId,
|
||||||
const newProcessedIds = response.data.results
|
email: response.data.email,
|
||||||
.filter((result) => result.guestId)
|
isAdmin: response.data.isAdmin,
|
||||||
.map((result) => result.guestId);
|
});
|
||||||
|
|
||||||
if (newProcessedIds.length > 0) {
|
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
|
||||||
processedGuestIds.value = [
|
if (response.data.authenticated) {
|
||||||
...new Set([...processedGuestIds.value, ...newProcessedIds]),
|
// Сначала обновляем идентификаторы, чтобы иметь актуальные данные
|
||||||
];
|
await updateIdentities();
|
||||||
console.log('Updated processed guest IDs from results:', processedGuestIds.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Очищаем гостевые сообщения из localStorage после успешного связывания
|
// Если пользователь только что аутентифицировался или сменил аккаунт,
|
||||||
localStorage.removeItem('guestMessages');
|
// связываем гостевые сообщения с его аккаунтом
|
||||||
localStorage.removeItem('guestId');
|
if (!wasAuthenticated || (previousUserId && previousUserId !== response.data.userId)) {
|
||||||
|
// Немедленно связываем сообщения
|
||||||
|
const linkResult = await linkMessages();
|
||||||
|
console.log('Link messages result on auth change:', linkResult);
|
||||||
|
|
||||||
return {
|
// Если пользователь только что аутентифицировался через Telegram,
|
||||||
success: true,
|
// обновляем историю чата без перезагрузки страницы
|
||||||
processedIds: processedGuestIds.value,
|
if (response.data.authType === 'telegram' && previousAuthType !== 'telegram') {
|
||||||
};
|
console.log('Telegram auth detected, loading message history');
|
||||||
}
|
// Отправляем событие для загрузки истории чата
|
||||||
} catch (error) {
|
window.dispatchEvent(new CustomEvent('load-chat-history'));
|
||||||
console.error('Error linking messages:', error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
// Предполагаем, что бэкенд автоматически связывает сообщения
|
|
||||||
// Очищаем данные гостя локально
|
|
||||||
console.log('Assuming backend handles message linking. Clearing local guest data.');
|
|
||||||
localStorage.removeItem('guestMessages');
|
|
||||||
localStorage.removeItem('guestId');
|
|
||||||
// Добавляем текущий guestId в обработанные, чтобы не пытаться отправить его снова
|
|
||||||
if(localGuestId) {
|
|
||||||
updateProcessedGuestIds([localGuestId]);
|
|
||||||
}
|
|
||||||
return { success: true, message: 'Local guest data cleared.' };
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, message: 'Not authenticated' };
|
// Обновляем отображение подключенного состояния в UI
|
||||||
} catch (error) {
|
updateConnectionDisplay(true, response.data.authType, response.data);
|
||||||
console.error('Error in linkMessages:', error);
|
} else {
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkAuth = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get('/api/auth/check');
|
|
||||||
console.log('Auth check response:', response.data);
|
|
||||||
|
|
||||||
const wasAuthenticated = isAuthenticated.value;
|
|
||||||
const previousUserId = userId.value;
|
|
||||||
const previousAuthType = authType.value;
|
|
||||||
|
|
||||||
// Обновляем данные авторизации через updateAuth вместо прямого изменения
|
|
||||||
await updateAuth({
|
|
||||||
authenticated: response.data.authenticated,
|
|
||||||
authType: response.data.authType,
|
|
||||||
userId: response.data.userId,
|
|
||||||
address: response.data.address,
|
|
||||||
telegramId: response.data.telegramId,
|
|
||||||
email: response.data.email,
|
|
||||||
isAdmin: response.data.isAdmin,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
|
|
||||||
if (response.data.authenticated) {
|
|
||||||
// Сначала обновляем идентификаторы, чтобы иметь актуальные данные
|
|
||||||
await updateIdentities();
|
|
||||||
|
|
||||||
// Если пользователь только что аутентифицировался или сменил аккаунт,
|
|
||||||
// связываем гостевые сообщения с его аккаунтом
|
|
||||||
if (!wasAuthenticated || (previousUserId && previousUserId !== response.data.userId)) {
|
|
||||||
// Немедленно связываем сообщения
|
|
||||||
const linkResult = await linkMessages();
|
|
||||||
console.log('Link messages result on auth change:', linkResult);
|
|
||||||
|
|
||||||
// Если пользователь только что аутентифицировался через Telegram,
|
|
||||||
// обновляем историю чата без перезагрузки страницы
|
|
||||||
if (response.data.authType === 'telegram' && previousAuthType !== 'telegram') {
|
|
||||||
console.log('Telegram auth detected, loading message history');
|
|
||||||
// Отправляем событие для загрузки истории чата
|
|
||||||
window.dispatchEvent(new CustomEvent('load-chat-history'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем отображение подключенного состояния в UI
|
|
||||||
updateConnectionDisplay(true, response.data.authType, response.data);
|
|
||||||
} else {
|
|
||||||
// Обновляем отображение отключенного состояния
|
|
||||||
updateConnectionDisplay(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking auth:', error);
|
|
||||||
// В случае ошибки сбрасываем состояние аутентификации
|
|
||||||
updateConnectionDisplay(false);
|
|
||||||
return { authenticated: false };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const disconnect = async () => {
|
|
||||||
try {
|
|
||||||
// Удаляем все идентификаторы перед выходом
|
|
||||||
await axios.post('/api/auth/logout');
|
|
||||||
|
|
||||||
// Обновляем состояние в памяти
|
|
||||||
updateAuth({
|
|
||||||
authenticated: false,
|
|
||||||
authType: null,
|
|
||||||
userId: null,
|
|
||||||
address: null,
|
|
||||||
telegramId: null,
|
|
||||||
email: null,
|
|
||||||
isAdmin: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обновляем отображение отключенного состояния
|
// Обновляем отображение отключенного состояния
|
||||||
updateConnectionDisplay(false);
|
updateConnectionDisplay(false);
|
||||||
|
}
|
||||||
|
|
||||||
// Очищаем списки идентификаторов
|
return response.data;
|
||||||
identities.value = [];
|
} catch (error) {
|
||||||
processedGuestIds.value = [];
|
console.error('Error checking auth:', error);
|
||||||
|
// В случае ошибки сбрасываем состояние аутентификации
|
||||||
|
updateConnectionDisplay(false);
|
||||||
|
return { authenticated: false };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Очищаем localStorage полностью
|
const disconnect = async () => {
|
||||||
localStorage.removeItem('isAuthenticated');
|
try {
|
||||||
localStorage.removeItem('userId');
|
// Удаляем все идентификаторы перед выходом
|
||||||
localStorage.removeItem('address');
|
await axios.post('/api/auth/logout');
|
||||||
localStorage.removeItem('isAdmin');
|
|
||||||
localStorage.removeItem('guestId');
|
|
||||||
localStorage.removeItem('guestMessages');
|
|
||||||
localStorage.removeItem('telegramId');
|
|
||||||
localStorage.removeItem('email');
|
|
||||||
|
|
||||||
// Удаляем класс подключенного кошелька
|
// Обновляем состояние в памяти
|
||||||
|
updateAuth({
|
||||||
|
authenticated: false,
|
||||||
|
authType: null,
|
||||||
|
userId: null,
|
||||||
|
address: null,
|
||||||
|
telegramId: null,
|
||||||
|
email: null,
|
||||||
|
isAdmin: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем отображение отключенного состояния
|
||||||
|
updateConnectionDisplay(false);
|
||||||
|
|
||||||
|
// Очищаем списки идентификаторов
|
||||||
|
identities.value = [];
|
||||||
|
processedGuestIds.value = [];
|
||||||
|
|
||||||
|
// Очищаем localStorage полностью
|
||||||
|
localStorage.removeItem('isAuthenticated');
|
||||||
|
localStorage.removeItem('userId');
|
||||||
|
localStorage.removeItem('address');
|
||||||
|
localStorage.removeItem('isAdmin');
|
||||||
|
localStorage.removeItem('guestId');
|
||||||
|
localStorage.removeItem('guestMessages');
|
||||||
|
localStorage.removeItem('telegramId');
|
||||||
|
localStorage.removeItem('email');
|
||||||
|
|
||||||
|
// Удаляем класс подключенного кошелька
|
||||||
|
document.body.classList.remove('wallet-connected');
|
||||||
|
|
||||||
|
console.log('User disconnected successfully and all identifiers cleared');
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error disconnecting:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обновляем список обработанных guestIds
|
||||||
|
const updateProcessedGuestIds = (ids) => {
|
||||||
|
if (Array.isArray(ids)) {
|
||||||
|
processedGuestIds.value = [...new Set([...processedGuestIds.value, ...ids])].slice(-20);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для обновления отображения подключения в UI
|
||||||
|
const updateConnectionDisplay = (isConnected, authType, authData = {}) => {
|
||||||
|
try {
|
||||||
|
console.log('Updating connection display:', { isConnected, authType, authData });
|
||||||
|
|
||||||
|
if (isConnected) {
|
||||||
|
document.body.classList.add('wallet-connected');
|
||||||
|
|
||||||
|
const authDisplayEl = document.getElementById('auth-display');
|
||||||
|
if (authDisplayEl) {
|
||||||
|
let displayText = 'Подключено';
|
||||||
|
|
||||||
|
if (authType === 'wallet' && authData.address) {
|
||||||
|
const shortAddress = `${authData.address.substring(0, 6)}...${authData.address.substring(authData.address.length - 4)}`;
|
||||||
|
displayText = `Кошелек: <strong>${shortAddress}</strong>`;
|
||||||
|
} else if (authType === 'email' && authData.email) {
|
||||||
|
displayText = `Email: <strong>${authData.email}</strong>`;
|
||||||
|
} else if (authType === 'telegram' && authData.telegramId) {
|
||||||
|
displayText = `Telegram: <strong>${authData.telegramUsername || authData.telegramId}</strong>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
authDisplayEl.innerHTML = displayText;
|
||||||
|
authDisplayEl.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Скрываем кнопки авторизации и показываем кнопку выхода
|
||||||
|
const authButtonsEl = document.getElementById('auth-buttons');
|
||||||
|
const logoutButtonEl = document.getElementById('logout-button');
|
||||||
|
|
||||||
|
if (authButtonsEl) authButtonsEl.style.display = 'none';
|
||||||
|
if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block';
|
||||||
|
} else {
|
||||||
document.body.classList.remove('wallet-connected');
|
document.body.classList.remove('wallet-connected');
|
||||||
|
|
||||||
console.log('User disconnected successfully and all identifiers cleared');
|
// Скрываем отображение аутентификации
|
||||||
|
const authDisplayEl = document.getElementById('auth-display');
|
||||||
return { success: true };
|
if (authDisplayEl) {
|
||||||
} catch (error) {
|
authDisplayEl.style.display = 'none';
|
||||||
console.error('Error disconnecting:', error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Обновляем список обработанных guestIds
|
|
||||||
const updateProcessedGuestIds = (ids) => {
|
|
||||||
if (Array.isArray(ids)) {
|
|
||||||
processedGuestIds.value = [...new Set([...processedGuestIds.value, ...ids])];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для обновления отображения подключения в UI
|
|
||||||
const updateConnectionDisplay = (isConnected, authType, authData = {}) => {
|
|
||||||
try {
|
|
||||||
console.log('Updating connection display:', { isConnected, authType, authData });
|
|
||||||
|
|
||||||
if (isConnected) {
|
|
||||||
document.body.classList.add('wallet-connected');
|
|
||||||
|
|
||||||
const authDisplayEl = document.getElementById('auth-display');
|
|
||||||
if (authDisplayEl) {
|
|
||||||
let displayText = 'Подключено';
|
|
||||||
|
|
||||||
if (authType === 'wallet' && authData.address) {
|
|
||||||
const shortAddress = `${authData.address.substring(0, 6)}...${authData.address.substring(authData.address.length - 4)}`;
|
|
||||||
displayText = `Кошелек: <strong>${shortAddress}</strong>`;
|
|
||||||
} else if (authType === 'email' && authData.email) {
|
|
||||||
displayText = `Email: <strong>${authData.email}</strong>`;
|
|
||||||
} else if (authType === 'telegram' && authData.telegramId) {
|
|
||||||
displayText = `Telegram: <strong>${authData.telegramUsername || authData.telegramId}</strong>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
authDisplayEl.innerHTML = displayText;
|
|
||||||
authDisplayEl.style.display = 'inline-block';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Скрываем кнопки авторизации и показываем кнопку выхода
|
|
||||||
const authButtonsEl = document.getElementById('auth-buttons');
|
|
||||||
const logoutButtonEl = document.getElementById('logout-button');
|
|
||||||
|
|
||||||
if (authButtonsEl) authButtonsEl.style.display = 'none';
|
|
||||||
if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block';
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove('wallet-connected');
|
|
||||||
|
|
||||||
// Скрываем отображение аутентификации
|
|
||||||
const authDisplayEl = document.getElementById('auth-display');
|
|
||||||
if (authDisplayEl) {
|
|
||||||
authDisplayEl.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Показываем кнопки авторизации и скрываем кнопку выхода
|
|
||||||
const authButtonsEl = document.getElementById('auth-buttons');
|
|
||||||
const logoutButtonEl = document.getElementById('logout-button');
|
|
||||||
|
|
||||||
if (authButtonsEl) authButtonsEl.style.display = 'flex';
|
|
||||||
if (logoutButtonEl) logoutButtonEl.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating connection display:', error);
|
// Показываем кнопки авторизации и скрываем кнопку выхода
|
||||||
|
const authButtonsEl = document.getElementById('auth-buttons');
|
||||||
|
const logoutButtonEl = document.getElementById('logout-button');
|
||||||
|
|
||||||
|
if (authButtonsEl) authButtonsEl.style.display = 'flex';
|
||||||
|
if (logoutButtonEl) logoutButtonEl.style.display = 'none';
|
||||||
}
|
}
|
||||||
};
|
} catch (error) {
|
||||||
|
console.error('Error updating connection display:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await checkAuth();
|
await checkAuth();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Очищаем интервал при размонтировании компонента
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopIdentitiesPolling();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Связывает новый идентификатор с текущим аккаунтом пользователя
|
||||||
|
* @param {string} type - Тип идентификатора (wallet, email, telegram)
|
||||||
|
* @param {string} value - Значение идентификатора
|
||||||
|
* @returns {Promise<Object>} - Результат операции
|
||||||
|
*/
|
||||||
|
const linkIdentity = async (type, value) => {
|
||||||
|
const response = await axios.post('/api/link', {
|
||||||
|
type,
|
||||||
|
value,
|
||||||
});
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
// Очищаем интервал при размонтировании компонента
|
/**
|
||||||
onUnmounted(() => {
|
* Удаляет идентификатор пользователя
|
||||||
stopIdentitiesPolling();
|
* @param {string} provider - Тип идентификатора (wallet, email, telegram)
|
||||||
});
|
* @param {string} providerId - Значение идентификатора
|
||||||
|
* @returns {Promise<Object>} - Результат операции
|
||||||
|
*/
|
||||||
|
const deleteIdentity = async (provider, providerId) => {
|
||||||
|
const response = await axios.delete(`/api/${provider}/${encodeURIComponent(providerId)}`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
// === SINGLETON API ===
|
||||||
* Связывает новый идентификатор с текущим аккаунтом пользователя
|
const authApi = {
|
||||||
* @param {string} type - Тип идентификатора (wallet, email, telegram)
|
isAuthenticated,
|
||||||
* @param {string} value - Значение идентификатора
|
authType,
|
||||||
* @returns {Promise<Object>} - Результат операции
|
userId,
|
||||||
*/
|
address,
|
||||||
const linkIdentity = async (type, value) => {
|
isAdmin,
|
||||||
const response = await axios.post('/api/link', {
|
telegramId,
|
||||||
type,
|
email,
|
||||||
value,
|
identities,
|
||||||
});
|
processedGuestIds,
|
||||||
return response.data;
|
tokenBalances,
|
||||||
};
|
updateAuth,
|
||||||
|
checkAuth,
|
||||||
|
disconnect,
|
||||||
|
linkMessages,
|
||||||
|
updateIdentities,
|
||||||
|
updateProcessedGuestIds,
|
||||||
|
updateConnectionDisplay,
|
||||||
|
linkIdentity,
|
||||||
|
deleteIdentity,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
// === PROVIDE/INJECT HELPERS ===
|
||||||
* Удаляет идентификатор пользователя
|
const AUTH_KEY = Symbol('auth');
|
||||||
* @param {string} provider - Тип идентификатора (wallet, email, telegram)
|
|
||||||
* @param {string} providerId - Значение идентификатора
|
|
||||||
* @returns {Promise<Object>} - Результат операции
|
|
||||||
*/
|
|
||||||
const deleteIdentity = async (provider, providerId) => {
|
|
||||||
const response = await axios.delete(`/api/${provider}/${encodeURIComponent(providerId)}`);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
export function provideAuth() {
|
||||||
isAuthenticated,
|
provide(AUTH_KEY, authApi);
|
||||||
authType,
|
}
|
||||||
userId,
|
|
||||||
address,
|
export function useAuthContext() {
|
||||||
isAdmin,
|
const ctx = inject(AUTH_KEY);
|
||||||
telegramId,
|
if (!ctx) throw new Error('Auth context not provided!');
|
||||||
email,
|
return ctx;
|
||||||
identities,
|
}
|
||||||
processedGuestIds,
|
|
||||||
tokenBalances,
|
// === useAuth теперь просто возвращает singleton ===
|
||||||
updateAuth,
|
export function useAuth() {
|
||||||
checkAuth,
|
return authApi;
|
||||||
disconnect,
|
|
||||||
linkMessages,
|
|
||||||
updateIdentities,
|
|
||||||
updateProcessedGuestIds,
|
|
||||||
updateConnectionDisplay,
|
|
||||||
linkIdentity,
|
|
||||||
deleteIdentity,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ref, onUnmounted } from 'vue';
|
import { ref, onUnmounted } from 'vue';
|
||||||
import api from '../api/axios';
|
import api from '../api/axios';
|
||||||
import { useAuth } from './useAuth';
|
import { useAuthContext } from './useAuth';
|
||||||
import { useNotifications } from './useNotifications';
|
import { useNotifications } from './useNotifications';
|
||||||
|
|
||||||
export function useAuthFlow(options = {}) {
|
export function useAuthFlow(options = {}) {
|
||||||
const { onAuthSuccess } = options; // Callback после успешной аутентификации/привязки
|
const { onAuthSuccess } = options; // Callback после успешной аутентификации/привязки
|
||||||
|
|
||||||
const auth = useAuth();
|
const auth = useAuthContext();
|
||||||
const { showSuccessMessage, showErrorMessage } = useNotifications();
|
const { showSuccessMessage, showErrorMessage } = useNotifications();
|
||||||
|
|
||||||
// Состояния Telegram
|
// Состояния Telegram
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ref, watch, onUnmounted } from 'vue';
|
import { ref, watch, onUnmounted } from 'vue';
|
||||||
import { fetchTokenBalances } from '../services/tokens';
|
import { fetchTokenBalances } from '../services/tokens';
|
||||||
import { useAuth } from './useAuth'; // Предполагаем, что useAuth предоставляет identities
|
import { useAuthContext } from './useAuth'; // Предполагаем, что useAuth предоставляет identities
|
||||||
import eventBus from '../utils/eventBus';
|
import eventBus from '../utils/eventBus';
|
||||||
|
|
||||||
export function useTokenBalances() {
|
export function useTokenBalances() {
|
||||||
const auth = useAuth(); // Получаем доступ к состоянию аутентификации
|
const auth = useAuthContext(); // Получаем доступ к состоянию аутентификации
|
||||||
const tokenBalances = ref([]); // теперь массив объектов
|
const tokenBalances = ref([]); // теперь массив объектов
|
||||||
const isLoadingTokens = ref(false);
|
const isLoadingTokens = ref(false);
|
||||||
let balanceUpdateInterval = null;
|
let balanceUpdateInterval = null;
|
||||||
|
|||||||
57
frontend/src/services/tablesService.js
Normal file
57
frontend/src/services/tablesService.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const api = '/api/tables';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async getTables() {
|
||||||
|
const res = await axios.get(`${api}?_t=${Date.now()}`);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
async createTable(data) {
|
||||||
|
const res = await axios.post(api, data);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
async getTable(id) {
|
||||||
|
const res = await axios.get(`${api}/${id}`);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
async addColumn(tableId, data) {
|
||||||
|
const res = await axios.post(`${api}/${tableId}/columns`, data);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
async addRow(tableId) {
|
||||||
|
const res = await axios.post(`${api}/${tableId}/rows`);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
async saveCell(data) {
|
||||||
|
const res = await axios.post(`${api}/cell`, data);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
async deleteColumn(columnId) {
|
||||||
|
const res = await axios.delete(`${api}/column/${columnId}`);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
async deleteRow(rowId) {
|
||||||
|
const res = await axios.delete(`${api}/row/${rowId}`);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
async updateColumn(columnId, data) {
|
||||||
|
const res = await axios.patch(`${api}/column/${columnId}`, data);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
async updateTable(id, data) {
|
||||||
|
const res = await axios.patch(`${api}/${id}`, data);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
async deleteTable(id) {
|
||||||
|
console.log('tablesService.deleteTable called with id:', id);
|
||||||
|
try {
|
||||||
|
const res = await axios.delete(`${api}/${id}`);
|
||||||
|
console.log('Delete response:', res.data);
|
||||||
|
return res.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in deleteTable service:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -22,13 +22,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<ContactTable v-if="showContacts" :contacts="contacts" @close="showContacts = false" @show-details="openContactDetails" />
|
<ContactTable v-if="showContacts" :contacts="contacts" @close="showContacts = false" @show-details="openContactDetails" />
|
||||||
<ContactDetails v-if="showContactDetails" :contact="selectedContact" @close="showContactDetails = false" @contact-deleted="onContactDeleted" />
|
<ContactDetails v-if="showContactDetails" :contact="selectedContact" @close="showContactDetails = false" @contact-deleted="onContactDeleted" />
|
||||||
|
<div class="crm-tables-block">
|
||||||
|
<h2>Таблицы</h2>
|
||||||
|
<button class="btn btn-info" @click="showTables = true">
|
||||||
|
<i class="fas fa-table"></i> Подробнее
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<DynamicTablesModal v-if="showTables" @close="showTables = false" />
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount, defineProps, defineEmits, computed, watch } from 'vue';
|
import { ref, onMounted, onBeforeUnmount, defineProps, defineEmits, computed, watch } from 'vue';
|
||||||
import { useAuth } from '../composables/useAuth';
|
import { useAuthContext } from '../composables/useAuth';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { setToStorage } from '../utils/storage';
|
import { setToStorage } from '../utils/storage';
|
||||||
import BaseLayout from '../components/BaseLayout.vue';
|
import BaseLayout from '../components/BaseLayout.vue';
|
||||||
@@ -38,6 +45,7 @@ import ContactTable from '../components/ContactTable.vue';
|
|||||||
import contactsService from '../services/contactsService.js';
|
import contactsService from '../services/contactsService.js';
|
||||||
import DleManagement from '../components/DleManagement.vue';
|
import DleManagement from '../components/DleManagement.vue';
|
||||||
import ContactDetails from '../components/ContactDetails.vue';
|
import ContactDetails from '../components/ContactDetails.vue';
|
||||||
|
import DynamicTablesModal from '../components/tables/DynamicTablesModal.vue';
|
||||||
|
|
||||||
// Определяем props
|
// Определяем props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -50,7 +58,7 @@ const props = defineProps({
|
|||||||
// Определяем emits
|
// Определяем emits
|
||||||
const emit = defineEmits(['auth-action-completed']);
|
const emit = defineEmits(['auth-action-completed']);
|
||||||
|
|
||||||
const auth = useAuth();
|
const auth = useAuthContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
const dleList = ref([]);
|
const dleList = ref([]);
|
||||||
@@ -62,6 +70,7 @@ const contacts = ref([]);
|
|||||||
const isLoadingContacts = ref(false);
|
const isLoadingContacts = ref(false);
|
||||||
const selectedContact = ref(null);
|
const selectedContact = ref(null);
|
||||||
const showContactDetails = ref(false);
|
const showContactDetails = ref(false);
|
||||||
|
const showTables = ref(false);
|
||||||
|
|
||||||
// Функция для перехода на домашнюю страницу и открытия боковой панели
|
// Функция для перехода на домашнюю страницу и открытия боковой панели
|
||||||
const goToHomeAndShowSidebar = () => {
|
const goToHomeAndShowSidebar = () => {
|
||||||
@@ -281,4 +290,24 @@ strong {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding: 8px 18px;
|
padding: 8px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.crm-tables-block {
|
||||||
|
margin: 32px 0 24px 0;
|
||||||
|
padding: 24px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.crm-tables-block h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.crm-tables-block .btn {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 8px 18px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits } from 'vue';
|
import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits } from 'vue';
|
||||||
import { useAuth } from '../composables/useAuth';
|
import { useAuthContext } from '../composables/useAuth';
|
||||||
import { useChat } from '../composables/useChat';
|
import { useChat } from '../composables/useChat';
|
||||||
import { connectWithWallet } from '../services/wallet';
|
import { connectWithWallet } from '../services/wallet';
|
||||||
import eventBus from '../utils/eventBus';
|
import eventBus from '../utils/eventBus';
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
// 1. ИСПОЛЬЗОВАНИЕ COMPOSABLES
|
// 1. ИСПОЛЬЗОВАНИЕ COMPOSABLES
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
const auth = useAuth();
|
const auth = useAuthContext();
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// 2. СОСТОЯНИЯ КОМПОНЕНТА
|
// 2. СОСТОЯНИЯ КОМПОНЕНТА
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, onBeforeUnmount, computed, defineProps, defineEmits } from 'vue';
|
import { ref, onMounted, watch, onBeforeUnmount, computed, defineProps, defineEmits } from 'vue';
|
||||||
import { useAuth } from '../composables/useAuth';
|
import { useAuthContext } from '../composables/useAuth';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { getFromStorage, setToStorage } from '../utils/storage';
|
import { getFromStorage, setToStorage } from '../utils/storage';
|
||||||
import BaseLayout from '../components/BaseLayout.vue';
|
import BaseLayout from '../components/BaseLayout.vue';
|
||||||
@@ -48,7 +48,7 @@ const props = defineProps({
|
|||||||
// Определяем emits
|
// Определяем emits
|
||||||
const emit = defineEmits(['auth-action-completed']);
|
const emit = defineEmits(['auth-action-completed']);
|
||||||
|
|
||||||
const auth = useAuth();
|
const auth = useAuthContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
|
|
||||||
|
|||||||
@@ -316,12 +316,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, onMounted, computed, ref, watch } from 'vue';
|
import { reactive, onMounted, computed, ref, watch } from 'vue';
|
||||||
import axios from 'axios'; // Предполагаем, что axios доступен
|
import axios from 'axios'; // Предполагаем, что axios доступен
|
||||||
import { useAuth } from '@/composables/useAuth'; // Импортируем composable useAuth
|
import { useAuthContext } from '@/composables/useAuth'; // Импортируем composable useAuth
|
||||||
import dleService from '@/services/dleService';
|
import dleService from '@/services/dleService';
|
||||||
import useBlockchainNetworks from '@/composables/useBlockchainNetworks'; // Импортируем composable для работы с сетями
|
import useBlockchainNetworks from '@/composables/useBlockchainNetworks'; // Импортируем composable для работы с сетями
|
||||||
// TODO: Импортировать API
|
// TODO: Импортировать API
|
||||||
|
|
||||||
const { address, isAdmin, auth, user } = useAuth(); // Получаем объект адреса и статус админа
|
const { address, isAdmin, auth, user } = useAuthContext(); // Получаем объект адреса и статус админа
|
||||||
|
|
||||||
// Инициализация composable для работы с сетями блокчейн
|
// Инициализация composable для работы с сетями блокчейн
|
||||||
const {
|
const {
|
||||||
|
|||||||
Reference in New Issue
Block a user