const sqlite3 = require('sqlite3').verbose(); const { Pool } = require('pg'); const path = require('path'); const fs = require('fs'); const initDocTables = require('./init-doc-tables'); require('dotenv').config(); // Определяем, какую базу использовать const USE_POSTGRES = process.env.POSTGRESQL === 'yes'; let db = null; // Основной объект базы данных let postgresPool = null; // Пул соединений PostgreSQL let isInitialized = false; // Флаг инициализации const dataDir = path.join(__dirname, 'data'); const createDirIfNotExists = (dirPath) => { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } }; createDirIfNotExists(dataDir); const dbPath = path.join(dataDir, 'school_crm.db'); const uploadsDir = path.join(dataDir, 'uploads'); const tasksDir = path.join(uploadsDir, 'tasks'); const logsDir = path.join(dataDir, 'logs'); createDirIfNotExists(uploadsDir); createDirIfNotExists(tasksDir); createDirIfNotExists(logsDir); // Инициализация базы данных async function initializeDatabase() { console.log(`🔧 Используется ${USE_POSTGRES ? 'PostgreSQL' : 'SQLite'}`); if (USE_POSTGRES) { // Используем PostgreSQL try { postgresPool = new Pool({ host: process.env.DB_HOST, port: process.env.DB_PORT || 5432, database: process.env.DB_NAME || 'minicrm', user: process.env.DB_USER, password: process.env.DB_PASSWORD, max: 10, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, }); // Тестируем подключение const client = await postgresPool.connect(); await client.query('SELECT 1'); client.release(); console.log('✅ Подключение к PostgreSQL установлено'); // Создаем адаптер для PostgreSQL db = createPostgresAdapter(postgresPool); // Проверяем и создаем таблицы await createPostgresTables(); isInitialized = true; } catch (error) { console.error('❌ Ошибка подключения к PostgreSQL:', error.message); console.log('🔄 Пытаемся использовать SQLite как запасной вариант...'); await initializeSQLite(); } } else { // Используем SQLite await initializeSQLite(); } // Инициализируем таблицы для документов (после создания основных таблиц) try { await initDocTables(db); } catch (error) { console.error('⚠️ Ошибка инициализации таблиц документов:', error.message); } // Синхронизируем группы пользователей await syncUserGroups(); return db; } function initializeSQLite() { return new Promise((resolve, reject) => { db = new sqlite3.Database(dbPath, (err) => { if (err) { console.error('❌ Ошибка подключения к SQLite:', err.message); reject(err); return; } else { console.log('✅ Подключение к SQLite установлено'); console.log('📁 База данных расположена:', dbPath); // Используем serialize для последовательного выполнения db.serialize(() => { // Создаем основные таблицы createSQLiteTables(); // Инициализируем таблицы для документов initDocTables(db); // Добавляем группы по умолчанию addDefaultGroups(); isInitialized = true; resolve(db); }); } }); }); } function initializeSQLite() { return new Promise((resolve, reject) => { db = new sqlite3.Database(dbPath, (err) => { if (err) { console.error('❌ Ошибка подключения к SQLite:', err.message); reject(err); return; } else { console.log('✅ Подключение к SQLite установлено'); console.log('📁 База данных расположена:', dbPath); createSQLiteTables(); isInitialized = true; resolve(db); } }); }); } // Функция для синхронизации групп пользователей из старой структуры в новую async function syncUserGroups() { console.log('🔄 Синхронизация групп пользователей...'); try { // Получаем всех пользователей const users = await new Promise((resolve, reject) => { db.all("SELECT id, groups FROM users WHERE groups IS NOT NULL AND groups != ''", [], (err, rows) => { if (err) reject(err); else resolve(rows || []); }); }); let syncedCount = 0; for (const user of users) { try { let groups = []; try { groups = JSON.parse(user.groups); } catch (e) { console.log(`⚠️ Не удалось распарсить группы для пользователя ${user.id}: ${user.groups}`); continue; } // Для каждой группы for (const groupName of groups) { if (!groupName || typeof groupName !== 'string') continue; // Находим ID группы const group = await new Promise((resolve, reject) => { db.get("SELECT id FROM user_groups WHERE name = ?", [groupName.trim()], (err, row) => { if (err) reject(err); else resolve(row); }); }); if (group) { // Добавляем пользователя в группу await new Promise((resolve, reject) => { db.run( `INSERT INTO user_group_memberships (user_id, group_id) VALUES (?, ?) ON CONFLICT (user_id, group_id) DO NOTHING`, [user.id, group.id], function(err) { if (err) reject(err); else resolve(); } ); }); console.log(`✅ Пользователь ${user.id} добавлен в группу "${groupName}"`); } else { console.log(`⚠️ Группа "${groupName}" не найдена для пользователя ${user.id}`); } } syncedCount++; } catch (error) { console.error(`❌ Ошибка синхронизации пользователя ${user.id}:`, error.message); } } console.log(`✅ Синхронизировано ${syncedCount} пользователей`); } catch (error) { console.error('❌ Ошибка синхронизации групп:', error); } } function createSQLiteTables() { // Таблица для истории уведомлений db.run(`CREATE TABLE IF NOT EXISTS notification_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, task_id INTEGER NOT NULL, notification_type TEXT NOT NULL, last_sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id), FOREIGN KEY (task_id) REFERENCES tasks (id), UNIQUE(user_id, task_id, notification_type) )`); // Таблица очереди email db.run(`CREATE TABLE IF NOT EXISTS email_queue ( id INTEGER PRIMARY KEY AUTOINCREMENT, to_email TEXT NOT NULL, subject TEXT NOT NULL, html_content TEXT NOT NULL, user_id INTEGER, task_id INTEGER, notification_type TEXT, retry_count INTEGER DEFAULT 0, status TEXT DEFAULT 'pending', error_message TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (task_id) REFERENCES tasks(id) )`); // Таблица настроек email (для хранения состояния блокировки) db.run(`CREATE TABLE IF NOT EXISTS email_settings ( setting_key TEXT PRIMARY KEY, setting_value TEXT, spam_blocked_until DATETIME, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); // Основные таблицы системы db.run(`CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, login TEXT UNIQUE NOT NULL, password TEXT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, role TEXT DEFAULT 'teacher', auth_type TEXT DEFAULT 'local', groups TEXT, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); db.run(`CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, status TEXT DEFAULT 'active', created_by INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, deleted_at DATETIME, deleted_by INTEGER, original_task_id INTEGER, start_date DATETIME, due_date DATETIME, rework_comment TEXT, closed_at DATETIME, closed_by INTEGER, task_type TEXT DEFAULT "regular", type TEXT, approver_group_id INTEGER, document_id INTEGER, FOREIGN KEY (created_by) REFERENCES users (id), FOREIGN KEY (deleted_by) REFERENCES users (id), FOREIGN KEY (original_task_id) REFERENCES tasks (id), FOREIGN KEY (closed_by) REFERENCES users (id) )`); db.run(`CREATE TABLE IF NOT EXISTS task_assignments ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, user_id INTEGER NOT NULL, status TEXT DEFAULT 'assigned', start_date DATETIME, due_date DATETIME, rework_comment TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (task_id) REFERENCES tasks (id), FOREIGN KEY (user_id) REFERENCES users (id) )`); db.run(`CREATE TABLE IF NOT EXISTS task_files ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, user_id INTEGER NOT NULL, filename TEXT NOT NULL, original_name TEXT NOT NULL, file_path TEXT NOT NULL, file_size INTEGER NOT NULL, uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (task_id) REFERENCES tasks (id), FOREIGN KEY (user_id) REFERENCES users (id) )`); db.run(`CREATE TABLE IF NOT EXISTS activity_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, user_id INTEGER NOT NULL, action TEXT NOT NULL, details TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (task_id) REFERENCES tasks (id), FOREIGN KEY (user_id) REFERENCES users (id) )`); db.run(`CREATE TABLE IF NOT EXISTS notification_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, notification_key TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); console.log('✅ База данных SQLite инициализирована'); // Таблица для пользовательских настроек db.run(`CREATE TABLE IF NOT EXISTS user_settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER UNIQUE NOT NULL, email_notifications BOOLEAN DEFAULT true, notification_email TEXT, telegram_notifications BOOLEAN DEFAULT false, telegram_chat_id TEXT, vk_notifications BOOLEAN DEFAULT false, vk_user_id TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) )`); console.log('✅ Таблица для пользовательских настроек инициализирована'); // Таблица для типов документов db.run(`CREATE TABLE IF NOT EXISTS document_types ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, description TEXT, template_path TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); // Таблица для документов db.run(`CREATE TABLE IF NOT EXISTS documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, document_type_id INTEGER NOT NULL, status TEXT DEFAULT 'draft', created_by INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, approved_at DATETIME, approved_by INTEGER, rejected_at DATETIME, rejected_by INTEGER, rejection_reason TEXT, file_path TEXT, file_name TEXT, file_size INTEGER, version INTEGER DEFAULT 1, parent_document_id INTEGER, FOREIGN KEY (document_type_id) REFERENCES document_types (id), FOREIGN KEY (created_by) REFERENCES users (id), FOREIGN KEY (approved_by) REFERENCES users (id), FOREIGN KEY (rejected_by) REFERENCES users (id), FOREIGN KEY (parent_document_id) REFERENCES documents (id) )`); // Таблица для этапов согласования db.run(`CREATE TABLE IF NOT EXISTS approval_stages ( id INTEGER PRIMARY KEY AUTOINCREMENT, document_type_id INTEGER NOT NULL, stage_number INTEGER NOT NULL, stage_name TEXT NOT NULL, approver_role TEXT, approver_user_id INTEGER, is_required BOOLEAN DEFAULT true, can_edit BOOLEAN DEFAULT false, can_comment BOOLEAN DEFAULT true, deadline_days INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(document_type_id, stage_number), FOREIGN KEY (document_type_id) REFERENCES document_types (id), FOREIGN KEY (approver_user_id) REFERENCES users (id) )`); // Таблица для согласования документов db.run(`CREATE TABLE IF NOT EXISTS document_approvals ( id INTEGER PRIMARY KEY AUTOINCREMENT, document_id INTEGER NOT NULL, stage_id INTEGER NOT NULL, approver_user_id INTEGER NOT NULL, status TEXT DEFAULT 'pending', comments TEXT, approved_at DATETIME, rejected_at DATETIME, deadline DATETIME, notified_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(document_id, stage_id, approver_user_id), FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE, FOREIGN KEY (stage_id) REFERENCES approval_stages (id), FOREIGN KEY (approver_user_id) REFERENCES users (id) )`); // Таблица для комментариев к документам db.run(`CREATE TABLE IF NOT EXISTS document_comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, document_id INTEGER NOT NULL, user_id INTEGER NOT NULL, comment TEXT NOT NULL, is_internal BOOLEAN DEFAULT false, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users (id) )`); // Таблица для истории изменений документов db.run(`CREATE TABLE IF NOT EXISTS document_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, document_id INTEGER NOT NULL, user_id INTEGER NOT NULL, action TEXT NOT NULL, changes TEXT, version INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users (id) )`); console.log('✅ Таблицы для согласования документов созданы'); // Таблицы для групп пользователей db.run(`CREATE TABLE IF NOT EXISTS user_groups ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, description TEXT, color TEXT DEFAULT '#3498db', can_approve_documents BOOLEAN DEFAULT false, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); db.run(`CREATE TABLE IF NOT EXISTS user_group_memberships ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, group_id INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, group_id), FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, FOREIGN KEY (group_id) REFERENCES user_groups (id) ON DELETE CASCADE )`); console.log('✅ Таблицы для групп пользователей созданы'); // Таблица для типов документов (упрощенная версия) db.run(`CREATE TABLE IF NOT EXISTS simple_document_types ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); // Таблица для документов (расширенная задача) db.run(`CREATE TABLE IF NOT EXISTS simple_documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, document_type_id INTEGER, document_number TEXT, document_date DATE, pages_count INTEGER, urgency_level TEXT CHECK(urgency_level IN ('normal', 'urgent', 'very_urgent')), comment TEXT, refusal_reason TEXT, FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE, FOREIGN KEY (document_type_id) REFERENCES simple_document_types(id) )`); console.log('✅ Таблицы для типов документов и документов (расширенные задачи) созданы'); // Таблица для сообщений чата задач db.run(`CREATE TABLE IF NOT EXISTS task_chat_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, user_id INTEGER NOT NULL, message TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, is_edited BOOLEAN DEFAULT false, is_deleted BOOLEAN DEFAULT false, reply_to_id INTEGER, FOREIGN KEY (task_id) REFERENCES tasks (id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, FOREIGN KEY (reply_to_id) REFERENCES task_chat_messages (id) ON DELETE SET NULL )`); // Таблица для файлов в сообщениях db.run(`CREATE TABLE IF NOT EXISTS task_chat_files ( id INTEGER PRIMARY KEY AUTOINCREMENT, message_id INTEGER NOT NULL, file_path TEXT NOT NULL, original_name TEXT NOT NULL, file_size INTEGER NOT NULL, file_type TEXT, uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (message_id) REFERENCES task_chat_messages (id) ON DELETE CASCADE )`); // Таблица для прочитанных сообщений db.run(`CREATE TABLE IF NOT EXISTS task_chat_reads ( id INTEGER PRIMARY KEY AUTOINCREMENT, message_id INTEGER NOT NULL, user_id INTEGER NOT NULL, read_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(message_id, user_id), FOREIGN KEY (message_id) REFERENCES task_chat_messages (id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE )`); console.log('✅ Таблица для сообщений чата задач созданы'); // Создаем индексы для улучшения производительности createSQLiteIndexes(); // Запускаем проверку и обновление структуры таблиц setTimeout(() => { checkAndUpdateTableStructure(); }, 2000); // Добавляем группы по умолчанию setTimeout(() => { addDefaultGroups(); }, 1000); } // Добавляем группы по умолчанию function addDefaultGroups() { const defaultGroups = [ { name: 'Администрация', description: 'Пользователи с правами администратора системы', color: '#e74c3c', can_approve_documents: true }, { name: 'Секретарь', description: 'Группа для согласования документов', color: '#3498db', can_approve_documents: true }, { name: 'help', description: 'Группа для получения заявок поддержки', color: '#27ae60', can_approve_documents: false }, { name: 'doc', description: 'Группа для работы с документами', color: '#9b59b6', can_approve_documents: false }, { name: 'ahch', description: 'Группа для AHCH задач', color: '#e67e22', can_approve_documents: false } ]; defaultGroups.forEach(group => { db.get("SELECT id FROM user_groups WHERE name = ?", [group.name], (err, existing) => { if (err) { console.error(`❌ Ошибка проверки группы ${group.name}:`, err.message); return; } if (!existing) { db.run( `INSERT INTO user_groups (name, description, color, can_approve_documents) VALUES (?, ?, ?, ?)`, [group.name, group.description, group.color, group.can_approve_documents ? 1 : 0], (insertErr) => { if (insertErr) { console.error(`❌ Ошибка создания группы ${group.name}:`, insertErr.message); } else { console.log(`✅ Группа "${group.name}" создана по умолчанию`); } } ); } }); }); } function createSQLiteIndexes() { console.log('🔧 Создаем индексы для SQLite...'); const indexes = [ // Индексы для очереди email "CREATE INDEX IF NOT EXISTS idx_email_queue_status ON email_queue(status, created_at)", "CREATE INDEX IF NOT EXISTS idx_email_queue_user_id ON email_queue(user_id)", "CREATE INDEX IF NOT EXISTS idx_email_queue_task_id ON email_queue(task_id)", // Индексы для уведомлений "CREATE INDEX IF NOT EXISTS idx_notification_history_user_id ON notification_history(user_id)", "CREATE INDEX IF NOT EXISTS idx_notification_history_task_id ON notification_history(task_id)", "CREATE INDEX IF NOT EXISTS idx_notification_history_type ON notification_history(notification_type)", "CREATE INDEX IF NOT EXISTS idx_notification_history_sent_at ON notification_history(last_sent_at)", // Индексы для пользователей "CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)", "CREATE INDEX IF NOT EXISTS idx_users_role ON users(role)", // Индексы для задач "CREATE INDEX IF NOT EXISTS idx_tasks_created_by ON tasks(created_by)", "CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)", "CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type)", "CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date)", "CREATE INDEX IF NOT EXISTS idx_tasks_closed_at ON tasks(closed_at)", // Индексы для назначений задач "CREATE INDEX IF NOT EXISTS idx_task_assignments_task_id ON task_assignments(task_id)", "CREATE INDEX IF NOT EXISTS idx_task_assignments_user_id ON task_assignments(user_id)", "CREATE INDEX IF NOT EXISTS idx_task_assignments_status ON task_assignments(status)", // Индексы для файлов "CREATE INDEX IF NOT EXISTS idx_task_files_task_id ON task_files(task_id)", // Индексы для логов активности "CREATE INDEX IF NOT EXISTS idx_activity_logs_task_id ON activity_logs(task_id)", "CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at)", // Индексы для настроек пользователей "CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)", // Индексы для групп "CREATE INDEX IF NOT EXISTS idx_user_groups_can_approve ON user_groups(can_approve_documents)", "CREATE INDEX IF NOT EXISTS idx_user_group_memberships_user_id ON user_group_memberships(user_id)", "CREATE INDEX IF NOT EXISTS idx_user_group_memberships_group_id ON user_group_memberships(group_id)", // Индексы для простых документов "CREATE INDEX IF NOT EXISTS idx_simple_documents_task_id ON simple_documents(task_id)", "CREATE INDEX IF NOT EXISTS idx_simple_documents_document_number ON simple_documents(document_number)", // Индексы для оптимизации "CREATE INDEX IF NOT EXISTS idx_task_chat_messages_task_id ON task_chat_messages(task_id)", "CREATE INDEX IF NOT EXISTS idx_task_chat_messages_created_at ON task_chat_messages(created_at)", "CREATE INDEX IF NOT EXISTS idx_task_chat_files_message_id ON task_chat_files(message_id)", "CREATE INDEX IF NOT EXISTS idx_task_chat_reads_message_id ON task_chat_reads(message_id)", "CREATE INDEX IF NOT EXISTS idx_task_chat_reads_user_id ON task_chat_reads(user_id)" ]; indexes.forEach(indexQuery => { db.run(indexQuery, (err) => { if (err) { console.error(`❌ Ошибка создания индекса: ${err.message}`); } else { console.log(`✅ Индекс создан: ${indexQuery.split('ON')[1]}`); } }); }); } // Функция для проверки и обновления структуры таблиц function checkAndUpdateTableStructure() { console.log('🔍 Проверка структуры таблиц...'); // Определяем ожидаемую структуру таблиц const tableSchemas = { users: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, { name: 'login', type: 'TEXT UNIQUE NOT NULL' }, { name: 'password', type: 'TEXT' }, { name: 'name', type: 'TEXT NOT NULL' }, { name: 'email', type: 'TEXT UNIQUE NOT NULL' }, { name: 'role', type: 'TEXT DEFAULT "teacher"' }, { name: 'auth_type', type: 'TEXT DEFAULT "local"' }, { name: 'groups', type: 'TEXT' }, { name: 'description', type: 'TEXT' }, { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, { name: 'last_login', type: 'DATETIME' }, { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } ], tasks: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, { name: 'title', type: 'TEXT NOT NULL' }, { name: 'description', type: 'TEXT' }, { name: 'status', type: 'TEXT DEFAULT "active"' }, { name: 'created_by', type: 'INTEGER NOT NULL' }, { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, { name: 'deleted_at', type: 'DATETIME' }, { name: 'deleted_by', type: 'INTEGER' }, { name: 'original_task_id', type: 'INTEGER' }, { name: 'start_date', type: 'DATETIME' }, { name: 'due_date', type: 'DATETIME' }, { name: 'rework_comment', type: 'TEXT' }, { name: 'closed_at', type: 'DATETIME' }, { name: 'closed_by', type: 'INTEGER' }, { name: 'task_type', type: 'TEXT DEFAULT "regular"' }, { name: 'type', type: 'TEXT' }, { name: 'approver_group_id', type: 'INTEGER' }, { name: 'document_id', type: 'INTEGER' } ], task_assignments: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, { name: 'task_id', type: 'INTEGER NOT NULL' }, { name: 'user_id', type: 'INTEGER NOT NULL' }, { name: 'status', type: 'TEXT DEFAULT "assigned"' }, { name: 'start_date', type: 'DATETIME' }, { name: 'due_date', type: 'DATETIME' }, { name: 'rework_comment', type: 'TEXT' }, { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } ], task_files: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, { name: 'task_id', type: 'INTEGER NOT NULL' }, { name: 'user_id', type: 'INTEGER NOT NULL' }, { name: 'filename', type: 'TEXT NOT NULL' }, { name: 'original_name', type: 'TEXT NOT NULL' }, { name: 'file_path', type: 'TEXT NOT NULL' }, { name: 'file_size', type: 'INTEGER NOT NULL' }, { name: 'uploaded_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } ], activity_logs: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, { name: 'task_id', type: 'INTEGER NOT NULL' }, { name: 'user_id', type: 'INTEGER NOT NULL' }, { name: 'action', type: 'TEXT NOT NULL' }, { name: 'details', type: 'TEXT' }, { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } ], notification_logs: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, { name: 'notification_key', type: 'TEXT NOT NULL' }, { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } ], user_settings: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, { name: 'user_id', type: 'INTEGER UNIQUE NOT NULL' }, { name: 'email_notifications', type: 'BOOLEAN DEFAULT true' }, { name: 'notification_email', type: 'TEXT' }, { name: 'telegram_notifications', type: 'BOOLEAN DEFAULT false' }, { name: 'telegram_chat_id', type: 'TEXT' }, { name: 'vk_notifications', type: 'BOOLEAN DEFAULT false' }, { name: 'vk_user_id', type: 'TEXT' }, { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } ], notification_history: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, { name: 'user_id', type: 'INTEGER NOT NULL' }, { name: 'task_id', type: 'INTEGER NOT NULL' }, { name: 'notification_type', type: 'TEXT NOT NULL' }, { name: 'last_sent_at', type: 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP' }, { name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' } ], email_queue: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, { name: 'to_email', type: 'TEXT NOT NULL' }, { name: 'subject', type: 'TEXT NOT NULL' }, { name: 'html_content', type: 'TEXT NOT NULL' }, { name: 'user_id', type: 'INTEGER' }, { name: 'task_id', type: 'INTEGER' }, { name: 'notification_type', type: 'TEXT' }, { name: 'retry_count', type: 'INTEGER DEFAULT 0' }, { name: 'status', type: 'TEXT DEFAULT "pending"' }, { name: 'error_message', type: 'TEXT' }, { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } ], email_settings: [ { name: 'setting_key', type: 'TEXT PRIMARY KEY' }, { name: 'setting_value', type: 'TEXT' }, { name: 'spam_blocked_until', type: 'DATETIME' }, { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } ], user_groups: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, { name: 'name', type: 'TEXT NOT NULL UNIQUE' }, { name: 'description', type: 'TEXT' }, { name: 'color', type: 'TEXT DEFAULT "#3498db"' }, { name: 'can_approve_documents', type: 'BOOLEAN DEFAULT false' }, { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } ], user_group_memberships: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, { name: 'user_id', type: 'INTEGER NOT NULL' }, { name: 'group_id', type: 'INTEGER NOT NULL' }, { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } ], simple_document_types: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, { name: 'name', type: 'TEXT NOT NULL' }, { name: 'description', type: 'TEXT' }, { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } ], simple_documents: [ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, { name: 'task_id', type: 'INTEGER NOT NULL' }, { name: 'document_type_id', type: 'INTEGER' }, { name: 'document_number', type: 'TEXT' }, { name: 'document_date', type: 'DATE' }, { name: 'pages_count', type: 'INTEGER' }, { name: 'urgency_level', type: 'TEXT CHECK(urgency_level IN (\'normal\', \'urgent\', \'very_urgent\'))' }, { name: 'comment', type: 'TEXT' }, { name: 'refusal_reason', type: 'TEXT' } ] }; // Проверяем каждую таблицу Object.entries(tableSchemas).forEach(([tableName, columns]) => { db.all(`PRAGMA table_info(${tableName})`, (err, existingColumns) => { if (err) { console.error(`❌ Ошибка проверки таблицы ${tableName}:`, err.message); return; } if (existingColumns.length === 0) { console.log(`⚠️ Таблица ${tableName} не существует, создаем...`); // Таблица будет создана автоматически при следующем запуске return; } // Создаем массив имен существующих колонок const existingColumnNames = existingColumns.map(col => col.name.toLowerCase()); // Проверяем каждую ожидаемую колонку columns.forEach(expectedColumn => { const expectedName = expectedColumn.name.toLowerCase(); if (!existingColumnNames.includes(expectedName)) { console.log(`🔧 Добавляем колонку ${expectedColumn.name} в таблицу ${tableName}...`); db.run( `ALTER TABLE ${tableName} ADD COLUMN ${expectedColumn.name} ${expectedColumn.type}`, (alterErr) => { if (alterErr) { console.error(`❌ Ошибка добавления колонки ${expectedColumn.name}:`, alterErr.message); } else { console.log(`✅ Колонка ${expectedColumn.name} добавлена в таблицу ${tableName}`); } } ); } }); }); }); // Создаем группу "Секретарь" по умолчанию, если её нет setTimeout(() => { db.get("SELECT id FROM user_groups WHERE name = 'Секретарь'", (err, group) => { if (err || !group) { console.log('🔧 Создаем группу "Секретарь" по умолчанию...'); db.run( `INSERT INTO user_groups (name, description, color, can_approve_documents) VALUES ('Секретарь', 'Группа для согласования документов', '#e74c3c', 1)`, (insertErr) => { if (insertErr) { console.error('❌ Ошибка создания группы "Секретарь":', insertErr.message); } else { console.log('✅ Группа "Секретарь" создана по умолчанию'); } } ); } }); }, 1000); } function createPostgresAdapter(pool) { // Адаптер для PostgreSQL с совместимым API return { all: (sql, params = [], callback) => { if (!callback && typeof params === 'function') { callback = params; params = []; } // Адаптируем SQL для PostgreSQL const adaptedSql = adaptSQLForPostgres(sql); pool.query(adaptedSql, params) .then(result => callback(null, result.rows)) .catch(err => { console.error('PostgreSQL Error (all):', err.message, 'SQL:', adaptedSql); callback(err); }); }, get: (sql, params = [], callback) => { if (!callback && typeof params === 'function') { callback = params; params = []; } // Адаптируем SQL для PostgreSQL const adaptedSql = adaptSQLForPostgres(sql); pool.query(adaptedSql, params) .then(result => callback(null, result.rows[0] || null)) .catch(err => { console.error('PostgreSQL Error (get):', err.message, 'SQL:', adaptedSql); callback(err); }); }, run: (sql, params = [], callback) => { if (!callback && typeof params === 'function') { callback = params; params = []; } // Адаптируем SQL для PostgreSQL const adaptedSql = adaptSQLForPostgres(sql); pool.query(adaptedSql, params) .then(result => { if (callback) { const lastIdQuery = sql.toLowerCase().includes('insert into') ? "SELECT lastval() as last_id" : "SELECT 0 as last_id"; if (sql.toLowerCase().includes('insert into')) { pool.query("SELECT lastval() as last_id", []) .then(lastIdResult => { callback(null, { lastID: lastIdResult.rows[0]?.last_id || null, changes: result.rowCount || 0 }); }) .catch(err => callback(err)); } else { callback(null, { lastID: null, changes: result.rowCount || 0 }); } } }) .catch(err => { console.error('PostgreSQL Error (run):', err.message, 'SQL:', adaptedSql); if (callback) callback(err); }); }, // Для транзакций - эмуляция serialize: (callback) => { // В PostgreSQL транзакции обрабатываются по-другому // Здесь просто выполняем колбэк try { callback(); } catch (error) { console.error('Error in serialize:', error); } }, // Закрытие соединения close: (callback) => { pool.end() .then(() => { if (callback) callback(null); }) .catch(err => { if (callback) callback(err); }); }, // Дополнительные методы exec: (sql, callback) => { pool.query(sql) .then(() => { if (callback) callback(null); }) .catch(err => { if (callback) callback(err); }); } }; } function adaptSQLForPostgres(sql) { // Адаптируем SQL запросы для PostgreSQL let adaptedSql = sql; // Заменяем SQLite-специфичные синтаксисы adaptedSql = adaptedSql.replace(/AUTOINCREMENT/gi, 'SERIAL'); adaptedSql = adaptedSql.replace(/DATETIME/gi, 'TIMESTAMP'); adaptedSql = adaptedSql.replace(/INTEGER PRIMARY KEY/gi, 'SERIAL PRIMARY KEY'); adaptedSql = adaptedSql.replace(/datetime\('now'\)/gi, 'CURRENT_TIMESTAMP'); adaptedSql = adaptedSql.replace(/CURRENT_TIMESTAMP/gi, 'CURRENT_TIMESTAMP'); // Исправляем INSERT с возвратом ID if (adaptedSql.includes('INSERT INTO') && adaptedSql.includes('RETURNING id')) { adaptedSql = adaptedSql.replace('RETURNING id', 'RETURNING id'); } return adaptedSql; } async function createPostgresTables() { if (!USE_POSTGRES) return; try { const client = await postgresPool.connect(); console.log('🔧 Проверяем/создаем таблицы в PostgreSQL...'); // Таблица для истории уведомлений await client.query(` CREATE TABLE IF NOT EXISTS notification_history ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), task_id INTEGER NOT NULL REFERENCES tasks(id), notification_type VARCHAR(50) NOT NULL, last_sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, task_id, notification_type) ) `); // Таблица очереди email await client.query(` CREATE TABLE IF NOT EXISTS email_queue ( id SERIAL PRIMARY KEY, to_email VARCHAR(500) NOT NULL, subject VARCHAR(500) NOT NULL, html_content TEXT NOT NULL, user_id INTEGER REFERENCES users(id), task_id INTEGER REFERENCES tasks(id), notification_type VARCHAR(50), retry_count INTEGER DEFAULT 0, status VARCHAR(50) DEFAULT 'pending', error_message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); // Таблица настроек email await client.query(` CREATE TABLE IF NOT EXISTS email_settings ( setting_key VARCHAR(100) PRIMARY KEY, setting_value TEXT, spam_blocked_until TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); // Основные таблицы await client.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, login VARCHAR(100) UNIQUE NOT NULL, password TEXT, name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, role VARCHAR(50) DEFAULT 'teacher', auth_type VARCHAR(50) DEFAULT 'local', groups TEXT DEFAULT '[]', description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); await client.query(` CREATE TABLE IF NOT EXISTS tasks ( id SERIAL PRIMARY KEY, title VARCHAR(500) NOT NULL, description TEXT, status VARCHAR(50) DEFAULT 'active', created_by INTEGER NOT NULL REFERENCES users(id), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, deleted_by INTEGER REFERENCES users(id), original_task_id INTEGER REFERENCES tasks(id), start_date TIMESTAMP, due_date TIMESTAMP, rework_comment TEXT, closed_at TIMESTAMP, closed_by INTEGER REFERENCES users(id), task_type VARCHAR(50) DEFAULT 'regular', type VARCHAR(100), approver_group_id INTEGER, document_id INTEGER ) `); await client.query(` CREATE TABLE IF NOT EXISTS task_assignments ( id SERIAL PRIMARY KEY, task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id), status VARCHAR(50) DEFAULT 'assigned', start_date TIMESTAMP, due_date TIMESTAMP, rework_comment TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); await client.query(` CREATE TABLE IF NOT EXISTS task_files ( id SERIAL PRIMARY KEY, task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id), filename VARCHAR(255) NOT NULL, original_name VARCHAR(500) NOT NULL, file_path TEXT NOT NULL, file_size BIGINT NOT NULL, uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); await client.query(` CREATE TABLE IF NOT EXISTS activity_logs ( id SERIAL PRIMARY KEY, task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id), action VARCHAR(100) NOT NULL, details TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); await client.query(` CREATE TABLE IF NOT EXISTS notification_logs ( id SERIAL PRIMARY KEY, notification_key VARCHAR(500) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); // Таблица для пользовательских настроек await client.query(` CREATE TABLE IF NOT EXISTS user_settings ( id SERIAL PRIMARY KEY, user_id INTEGER UNIQUE NOT NULL REFERENCES users(id), email_notifications BOOLEAN DEFAULT true, notification_email TEXT, telegram_notifications BOOLEAN DEFAULT false, telegram_chat_id TEXT, vk_notifications BOOLEAN DEFAULT false, vk_user_id TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); // Таблицы для групп пользователей await client.query(` CREATE TABLE IF NOT EXISTS user_groups ( id SERIAL PRIMARY KEY, name VARCHAR(100) UNIQUE NOT NULL, description TEXT, color VARCHAR(20) DEFAULT '#3498db', can_approve_documents BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); await client.query(` CREATE TABLE IF NOT EXISTS user_group_memberships ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, group_id INTEGER NOT NULL REFERENCES user_groups(id) ON DELETE CASCADE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, group_id) ) `); // Таблицы для документов await client.query(` CREATE TABLE IF NOT EXISTS document_types ( id SERIAL PRIMARY KEY, name VARCHAR(100) UNIQUE NOT NULL, description TEXT, template_path TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); await client.query(` CREATE TABLE IF NOT EXISTS documents ( id SERIAL PRIMARY KEY, title VARCHAR(500) NOT NULL, description TEXT, document_type_id INTEGER NOT NULL REFERENCES document_types(id), status VARCHAR(50) DEFAULT 'draft', created_by INTEGER NOT NULL REFERENCES users(id), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, approved_at TIMESTAMP, approved_by INTEGER REFERENCES users(id), rejected_at TIMESTAMP, rejected_by INTEGER REFERENCES users(id), rejection_reason TEXT, file_path TEXT, file_name TEXT, file_size BIGINT, version INTEGER DEFAULT 1, parent_document_id INTEGER REFERENCES documents(id) ) `); await client.query(` CREATE TABLE IF NOT EXISTS approval_stages ( id SERIAL PRIMARY KEY, document_type_id INTEGER NOT NULL REFERENCES document_types(id), stage_number INTEGER NOT NULL, stage_name VARCHAR(100) NOT NULL, approver_role VARCHAR(50), approver_user_id INTEGER REFERENCES users(id), is_required BOOLEAN DEFAULT true, can_edit BOOLEAN DEFAULT false, can_comment BOOLEAN DEFAULT true, deadline_days INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(document_type_id, stage_number) ) `); await client.query(` CREATE TABLE IF NOT EXISTS document_approvals ( id SERIAL PRIMARY KEY, document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE, stage_id INTEGER NOT NULL REFERENCES approval_stages(id), approver_user_id INTEGER NOT NULL REFERENCES users(id), status VARCHAR(50) DEFAULT 'pending', comments TEXT, approved_at TIMESTAMP, rejected_at TIMESTAMP, deadline TIMESTAMP, notified_at TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(document_id, stage_id, approver_user_id) ) `); await client.query(` CREATE TABLE IF NOT EXISTS document_comments ( id SERIAL PRIMARY KEY, document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id), comment TEXT NOT NULL, is_internal BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); await client.query(` CREATE TABLE IF NOT EXISTS document_history ( id SERIAL PRIMARY KEY, document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id), action VARCHAR(100) NOT NULL, changes TEXT, version INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); // Таблица для типов документов (упрощенная версия) await client.query(` CREATE TABLE IF NOT EXISTS simple_document_types ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); // Таблица для документов (расширенная задача) await client.query(` CREATE TABLE IF NOT EXISTS simple_documents ( id SERIAL PRIMARY KEY, task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, document_type_id INTEGER REFERENCES simple_document_types(id), document_number VARCHAR(100), document_date DATE, pages_count INTEGER, urgency_level VARCHAR(20) CHECK (urgency_level IN ('normal', 'urgent', 'very_urgent')), comment TEXT, refusal_reason TEXT ) `); console.log('✅ Все таблицы PostgreSQL созданы/проверены'); // Создаем индексы await createPostgresIndexes(client); // Добавляем группы по умолчанию для PostgreSQL await addDefaultGroupsPostgreSQL(client); client.release(); console.log('✅ Таблицы PostgreSQL проверены/созданы'); // Проверяем структуру PostgreSQL таблиц await checkPostgresTableStructure(); } catch (error) { console.error('❌ Ошибка создания таблиц PostgreSQL:', error.message); } } // Добавляем группы по умолчанию для PostgreSQL async function addDefaultGroupsPostgreSQL(client) { const defaultGroups = [ { name: 'Администрация', description: 'Пользователи с правами администратора системы', color: '#e74c3c', can_approve_documents: true }, { name: 'Секретарь', description: 'Группа для согласования документов', color: '#3498db', can_approve_documents: true }, { name: 'help', description: 'Группа для получения заявок поддержки', color: '#27ae60', can_approve_documents: false }, { name: 'doc', description: 'Группа для работы с документами', color: '#9b59b6', can_approve_documents: false }, { name: 'ahch', description: 'Группа для AHCH задач', color: '#e67e22', can_approve_documents: false } ]; for (const group of defaultGroups) { try { const checkResult = await client.query( "SELECT id FROM user_groups WHERE name = $1", [group.name] ); if (checkResult.rows.length === 0) { await client.query( `INSERT INTO user_groups (name, description, color, can_approve_documents) VALUES ($1, $2, $3, $4)`, [group.name, group.description, group.color, group.can_approve_documents] ); console.log(`✅ Группа "${group.name}" создана по умолчанию в PostgreSQL`); } } catch (error) { console.error(`❌ Ошибка создания группы ${group.name}:`, error.message); } } } async function createPostgresIndexes(client) { console.log('🔧 Создаем индексы для PostgreSQL...'); const indexes = [ // Индексы для очереди email 'CREATE INDEX IF NOT EXISTS idx_email_queue_status ON email_queue(status, created_at)', 'CREATE INDEX IF NOT EXISTS idx_email_queue_user_id ON email_queue(user_id)', 'CREATE INDEX IF NOT EXISTS idx_email_queue_task_id ON email_queue(task_id)', // Индексы для уведомлений 'CREATE INDEX IF NOT EXISTS idx_notification_history_user_id ON notification_history(user_id)', 'CREATE INDEX IF NOT EXISTS idx_notification_history_task_id ON notification_history(task_id)', 'CREATE INDEX IF NOT EXISTS idx_notification_history_type ON notification_history(notification_type)', 'CREATE INDEX IF NOT EXISTS idx_notification_history_sent_at ON notification_history(last_sent_at)', // Индексы для пользователей 'CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)', 'CREATE INDEX IF NOT EXISTS idx_users_role ON users(role)', // Индексы для задач 'CREATE INDEX IF NOT EXISTS idx_tasks_created_by ON tasks(created_by)', 'CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)', 'CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type)', 'CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date)', 'CREATE INDEX IF NOT EXISTS idx_tasks_closed_at ON tasks(closed_at)', // Индексы для назначений задач 'CREATE INDEX IF NOT EXISTS idx_task_assignments_task_id ON task_assignments(task_id)', 'CREATE INDEX IF NOT EXISTS idx_task_assignments_user_id ON task_assignments(user_id)', 'CREATE INDEX IF NOT EXISTS idx_task_assignments_status ON task_assignments(status)', // Индексы для файлов 'CREATE INDEX IF NOT EXISTS idx_task_files_task_id ON task_files(task_id)', // Индексы для логов активности 'CREATE INDEX IF NOT EXISTS idx_activity_logs_task_id ON activity_logs(task_id)', 'CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at)', // Индексы для настроек пользователей 'CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)', // Индексы для групп 'CREATE INDEX IF NOT EXISTS idx_user_groups_can_approve ON user_groups(can_approve_documents)', 'CREATE INDEX IF NOT EXISTS idx_user_group_memberships_user_id ON user_group_memberships(user_id)', 'CREATE INDEX IF NOT EXISTS idx_user_group_memberships_group_id ON user_group_memberships(group_id)', // Индексы для простых документов 'CREATE INDEX IF NOT EXISTS idx_simple_documents_task_id ON simple_documents(task_id)', 'CREATE INDEX IF NOT EXISTS idx_simple_documents_document_number ON simple_documents(document_number)' ]; for (const indexQuery of indexes) { try { await client.query(indexQuery); } catch (err) { console.warn(`⚠️ Не удалось создать индекс: ${err.message}`); } } } // Функция для проверки структуры таблиц PostgreSQL async function checkPostgresTableStructure() { if (!USE_POSTGRES) return; try { const client = await postgresPool.connect(); console.log('🔍 Проверка структуры таблиц PostgreSQL...'); // Определяем ожидаемую структуру таблиц PostgreSQL const tableSchemas = { email_queue: [ { name: 'to_email', type: 'VARCHAR(500) NOT NULL' }, { name: 'subject', type: 'VARCHAR(500) NOT NULL' }, { name: 'html_content', type: 'TEXT NOT NULL' }, { name: 'user_id', type: 'INTEGER REFERENCES users(id)' }, { name: 'task_id', type: 'INTEGER REFERENCES tasks(id)' }, { name: 'notification_type', type: 'VARCHAR(50)' }, { name: 'retry_count', type: 'INTEGER DEFAULT 0' }, { name: 'status', type: 'VARCHAR(50) DEFAULT \'pending\'' }, { name: 'error_message', type: 'TEXT' }, { name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }, { name: 'updated_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' } ], email_settings: [ { name: 'setting_key', type: 'VARCHAR(100) PRIMARY KEY' }, { name: 'setting_value', type: 'TEXT' }, { name: 'spam_blocked_until', type: 'TIMESTAMP' }, { name: 'updated_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' } ], notification_history: [ { name: 'user_id', type: 'INTEGER NOT NULL REFERENCES users(id)' }, { name: 'task_id', type: 'INTEGER NOT NULL REFERENCES tasks(id)' }, { name: 'notification_type', type: 'VARCHAR(50) NOT NULL' }, { name: 'last_sent_at', type: 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP' }, { name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' } ] }; // Проверяем каждую таблицу for (const [tableName, columns] of Object.entries(tableSchemas)) { try { // Проверяем существование таблицы const tableExists = await client.query( "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1)", [tableName] ); if (!tableExists.rows[0].exists) { console.log(`⚠️ Таблица ${tableName} не существует в PostgreSQL`); continue; } // Получаем существующие колонки const existingColumns = await client.query(` SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position `, [tableName]); const existingColumnNames = existingColumns.rows.map(col => col.column_name.toLowerCase()); // Проверяем каждую ожидаемую колонку for (const expectedColumn of columns) { const expectedName = expectedColumn.name.toLowerCase(); if (!existingColumnNames.includes(expectedName)) { console.log(`🔧 Добавляем колонку ${expectedColumn.name} в таблицу PostgreSQL ${tableName}...`); try { await client.query( `ALTER TABLE ${tableName} ADD COLUMN ${expectedColumn.name} ${expectedColumn.type}` ); console.log(`✅ Колонка ${expectedColumn.name} добавлена в таблицу PostgreSQL ${tableName}`); } catch (alterErr) { console.error(`❌ Ошибка добавления колонки ${expectedColumn.name}:`, alterErr.message); } } } } catch (error) { console.error(`❌ Ошибка проверки таблицы PostgreSQL ${tableName}:`, error.message); } } client.release(); console.log('✅ Проверка структуры таблиц PostgreSQL завершена'); } catch (error) { console.error('❌ Ошибка проверки структуры таблиц PostgreSQL:', error.message); } } // Вспомогательные функции для работы с группами пользователей function getUserGroups(userId, callback) { const query = ` SELECT g.* FROM user_groups g JOIN user_group_memberships ugm ON g.id = ugm.group_id WHERE ugm.user_id = ? ORDER BY g.name `; db.all(query, [userId], (err, groups) => { if (err) { console.error('❌ Ошибка получения групп пользователя:', err); callback(err, []); } else { callback(null, groups || []); } }); } function getGroupMembers(groupId, callback) { const query = ` SELECT u.* FROM users u JOIN user_group_memberships ugm ON u.id = ugm.user_id WHERE ugm.group_id = ? ORDER BY u.name `; db.all(query, [groupId], (err, members) => { if (err) { console.error('❌ Ошибка получения участников группы:', err); callback(err, []); } else { callback(null, members || []); } }); } function getApproverGroups(callback) { const query = ` SELECT g.*, COUNT(DISTINCT ugm.user_id) as member_count FROM user_groups g LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id WHERE g.can_approve_documents = true GROUP BY g.id ORDER BY g.name `; db.all(query, [], (err, groups) => { if (err) { console.error('❌ Ошибка получения групп согласующих:', err); callback(err, []); } else { callback(null, groups || []); } }); } function addUserToGroup(userId, groupId, callback) { const query = ` INSERT INTO user_group_memberships (user_id, group_id) VALUES (?, ?) ON CONFLICT (user_id, group_id) DO NOTHING `; db.run(query, [userId, groupId], function(err) { if (err) { console.error('❌ Ошибка добавления пользователя в группу:', err); callback(err); } else { callback(null, this.changes > 0); } }); } function removeUserFromGroup(userId, groupId, callback) { const query = ` DELETE FROM user_group_memberships WHERE user_id = ? AND group_id = ? `; db.run(query, [userId, groupId], function(err) { if (err) { console.error('❌ Ошибка удаления пользователя из группы:', err); callback(err); } else { callback(null, this.changes > 0); } }); } function createTaskFolder(taskId) { const taskFolder = path.join(tasksDir, taskId.toString()); createDirIfNotExists(taskFolder); return taskFolder; } function createUserTaskFolder(taskId, userLogin) { const taskFolder = path.join(tasksDir, taskId.toString()); const userFolder = path.join(taskFolder, userLogin); createDirIfNotExists(userFolder); return userFolder; } function saveTaskMetadata(taskId, title, description, createdBy, originalTaskId = null, startDate = null, dueDate = null) { const taskFolder = createTaskFolder(taskId); const metadata = { id: taskId, title: title, description: description, status: 'active', created_by: createdBy, original_task_id: originalTaskId, start_date: startDate, due_date: dueDate, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), files: [] }; const metadataPath = path.join(taskFolder, 'task.json'); fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2)); } function updateTaskMetadata(taskId, updates) { const metadataPath = path.join(tasksDir, taskId.toString(), 'task.json'); if (fs.existsSync(metadataPath)) { const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8')); const updatedMetadata = { ...metadata, ...updates, updated_at: new Date().toISOString() }; fs.writeFileSync(metadataPath, JSON.stringify(updatedMetadata, null, 2)); } } function logActivity(taskId, userId, action, details = '') { db.run( "INSERT INTO activity_logs (task_id, user_id, action, details) VALUES (?, ?, ?, ?)", [taskId, userId, action, details] ); const logEntry = `${new Date().toISOString()} - User ${userId}: ${action} - Task ${taskId} - ${details}\n`; fs.appendFileSync(path.join(logsDir, 'activity.log'), logEntry); } function checkTaskAccess(userId, taskId, callback) { db.get("SELECT role FROM users WHERE id = ?", [userId], (err, user) => { if (err) { callback(err, false); return; } // Администратор имеет полный доступ if (user && user.role === 'admin') { callback(null, true); return; } if (user && user.role === 'tasks') { callback(null, true); return; } db.get("SELECT status, created_by, closed_at FROM tasks WHERE id = ?", [taskId], (err, task) => { if (err || !task) { callback(err, false); return; } if (task.closed_at && task.created_by !== userId && user.role !== 'admin') { callback(null, false); return; } const query = ` SELECT 1 FROM tasks t WHERE t.id = ? AND ( t.created_by = ? OR EXISTS (SELECT 1 FROM task_assignments WHERE task_id = t.id AND user_id = ?) ) `; db.get(query, [taskId, userId, userId], (err, row) => { callback(err, !!row); }); }); }); } function checkOverdueTasks() { const now = new Date().toISOString(); const query = ` SELECT ta.id, ta.task_id, ta.user_id, ta.status, ta.due_date FROM task_assignments ta JOIN tasks t ON ta.task_id = t.id WHERE ta.due_date IS NOT NULL AND ta.due_date < ? AND ta.status NOT IN ('completed', 'overdue') AND t.status = 'active' AND t.closed_at IS NULL `; db.all(query, [now], (err, assignments) => { if (err) { console.error('Ошибка при проверке просроченных задач:', err); return; } assignments.forEach(assignment => { db.run( "UPDATE task_assignments SET status = 'overdue' WHERE id = ?", [assignment.id] ); logActivity(assignment.task_id, assignment.user_id, 'STATUS_CHANGED', 'Задача просрочена'); }); }); } setInterval(checkOverdueTasks, 60000); // Функции для работы с простыми документами function createSimpleDocumentType(name, description, callback) { db.run( "INSERT INTO simple_document_types (name, description) VALUES (?, ?)", [name, description], function(err) { if (err) { console.error('❌ Ошибка создания типа документа:', err); callback(err); } else { callback(null, this.lastID); } } ); } function getSimpleDocumentTypes(callback) { db.all("SELECT * FROM simple_document_types ORDER BY name", [], (err, types) => { if (err) { console.error('❌ Ошибка получения типов документов:', err); callback(err, []); } else { callback(null, types || []); } }); } function createSimpleDocument(taskId, documentData, callback) { const { document_type_id, document_number, document_date, pages_count, urgency_level, comment, refusal_reason } = documentData; const query = ` INSERT INTO simple_documents ( task_id, document_type_id, document_number, document_date, pages_count, urgency_level, comment, refusal_reason ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; db.run(query, [ taskId, document_type_id, document_number, document_date, pages_count, urgency_level, comment, refusal_reason ], function(err) { if (err) { console.error('❌ Ошибка создания документа:', err); callback(err); } else { callback(null, this.lastID); } }); } function getTaskDocuments(taskId, callback) { const query = ` SELECT sd.*, sdt.name as document_type_name FROM simple_documents sd LEFT JOIN simple_document_types sdt ON sd.document_type_id = sdt.id WHERE sd.task_id = ? ORDER BY sd.document_date DESC, sd.id DESC `; db.all(query, [taskId], (err, documents) => { if (err) { console.error('❌ Ошибка получения документов задачи:', err); callback(err, []); } else { callback(null, documents || []); } }); } function updateSimpleDocument(documentId, updates, callback) { const fields = []; const values = []; Object.entries(updates).forEach(([key, value]) => { fields.push(`${key} = ?`); values.push(value); }); if (fields.length === 0) { callback(new Error('Нет полей для обновления')); return; } values.push(documentId); const query = `UPDATE simple_documents SET ${fields.join(', ')} WHERE id = ?`; db.run(query, values, function(err) { if (err) { console.error('❌ Ошибка обновления документа:', err); callback(err); } else { callback(null, this.changes > 0); } }); } function deleteSimpleDocument(documentId, callback) { db.run("DELETE FROM simple_documents WHERE id = ?", [documentId], function(err) { if (err) { console.error('❌ Ошибка удаления документа:', err); callback(err); } else { callback(null, this.changes > 0); } }); } module.exports = { initializeDatabase, // Экспортируем функцию инициализации getDb: () => { if (!isInitialized) { throw new Error('База данных не инициализирована'); } return db; }, isInitialized: () => isInitialized, logActivity, createTaskFolder, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess, USE_POSTGRES, getDatabaseType: () => USE_POSTGRES ? 'PostgreSQL' : 'SQLite', checkAndUpdateTableStructure, // Экспортируем для ручного запуска syncUserGroups, // Экспортируем функцию синхронизации // Функции для работы с группами getUserGroups, getGroupMembers, getApproverGroups, addUserToGroup, removeUserFromGroup, // Функции для работы с простыми документами createSimpleDocumentType, getSimpleDocumentTypes, createSimpleDocument, getTaskDocuments, updateSimpleDocument, deleteSimpleDocument }; // Запускаем инициализацию при экспорте (но она завершится позже) initializeDatabase().catch(err => { console.error('❌ Ошибка инициализации базы данных:', err.message); });