1867 lines
76 KiB
JavaScript
1867 lines
76 KiB
JavaScript
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('✅ Таблицы для типов документов и документов (расширенные задачи) созданы');
|
||
|
||
// Создаем индексы для улучшения производительности
|
||
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)"
|
||
];
|
||
|
||
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;
|
||
}
|
||
|
||
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);
|
||
}); |