diff --git a/public/style.css b/public/style.css index 7bd7f8d..504fbe7 100644 --- a/public/style.css +++ b/public/style.css @@ -4654,4 +4654,110 @@ button.btn-primary { word-break: break-word; max-width: 100%; } - } \ No newline at end of file + } +/* Стили для кнопки изменения срока */ +.change-deadline-btn { + background-color: #4a6fa5; + color: white; + border: none; + border-radius: 4px; + padding: 6px 12px; + margin: 0 4px; + cursor: pointer; + font-size: 13px; + display: inline-flex; + align-items: center; + gap: 4px; + transition: background-color 0.2s; +} + +.change-deadline-btn:hover { + background-color: #2c4a7a; +} + +/* Стили для модального окна */ +#change-deadline-modal .modal-content { + border-radius: 8px; + animation: slideIn 0.3s ease; +} + +#change-deadline-modal .form-group { + margin-bottom: 20px; +} + +#change-deadline-modal label { + display: block; + margin-bottom: 8px; + color: #333; + font-weight: 500; +} + +#change-deadline-modal .form-control { + width: 100%; + padding: 10px; + border: 2px solid #e1e5e9; + border-radius: 6px; + font-size: 14px; + transition: border-color 0.2s; +} + +#change-deadline-modal .form-control:focus { + border-color: #4a6fa5; + outline: none; +} + +#change-deadline-modal small { + display: block; + margin-top: 5px; + color: #666; + font-size: 12px; +} + +#change-deadline-modal .modal-footer { + border-top: 1px solid #e1e5e9; + padding-top: 20px; +} + +#change-deadline-modal .btn-primary { + background-color: #28a745; + color: white; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + font-weight: 500; +} + +#change-deadline-modal .btn-primary:hover:not(:disabled) { + background-color: #218838; +} + +#change-deadline-modal .btn-primary:disabled { + background-color: #6c757d; + cursor: not-allowed; +} + +#change-deadline-modal .btn-cancel { + background-color: #6c757d; + color: white; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + margin-right: 10px; +} + +#change-deadline-modal .btn-cancel:hover { + background-color: #5a6268; +} + +@keyframes slideIn { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} \ No newline at end of file diff --git a/public/ui.js b/public/ui.js index f24d1e3..a8a5697 100644 --- a/public/ui.js +++ b/public/ui.js @@ -212,7 +212,11 @@ ${currentUser && currentUser.role === 'tasks' && canEdit || currentUser.role === ${currentUser && currentUser.login === 'kalugin.o' ? `` : ''} ${task.task_type === 'document' && userRole === 'Исполнитель' ? `` : ''} - ${currentUser && currentUser.login === 'minicrm' ? `` : ''} + +${!isDeleted && !isClosed && task.task_type !== 'regular' && task.assignments && task.assignments.some(a => parseInt(a.user_id) === currentUser.id) ? +`` : ''} + +${currentUser && currentUser.login === 'minicrm' ? `` : ''} ${canEdit ? `` : ''} ` : ''} ${isClosed && canEdit ? ` @@ -1927,4 +1931,173 @@ function renderGroupedFiles(task) { `).join(''); -} \ No newline at end of file +} +// ++++++++++++++++++++++++++++++ кнопки изменения срока задачи для исполнителей ++++++++++++++++++++++++++++++ +// Функция для открытия модального окна изменения срока задачи +function openChangeDeadlineModal(taskId) { + const task = tasks.find(t => t.id === taskId); + if (!task) { + alert('Задача не найдена'); + return; + } + + // Проверяем, что задача не обычная (regular) + if (task.task_type === 'regular') { + alert('Для обычных задач изменение срока недоступно'); + return; + } + + // Проверяем, является ли пользователь исполнителем + const isExecutor = task.assignments && task.assignments.some(a => + parseInt(a.user_id) === currentUser.id + ); + + if (!isExecutor && currentUser.role !== 'admin') { + alert('Только исполнители могут изменять срок задачи'); + return; + } + + const modalHtml = ` + + `; + + const modalContainer = document.createElement('div'); + modalContainer.innerHTML = modalHtml; + document.body.appendChild(modalContainer); + + setTimeout(() => { + document.getElementById('change-deadline-modal').style.display = 'block'; + document.getElementById('deadline-change-comment').focus(); + }, 10); +} + +// Функция закрытия модального окна +function closeChangeDeadlineModal() { + const modal = document.getElementById('change-deadline-modal'); + if (modal) { + modal.style.display = 'none'; + setTimeout(() => { + modal.parentElement.remove(); + }, 300); + } +} + +// Функция отправки изменения срока +async function submitDeadlineChange(taskId) { + const newDate = document.getElementById('new-deadline-date').value; + const comment = document.getElementById('deadline-change-comment').value.trim(); + + if (!newDate) { + alert('Пожалуйста, выберите новую дату'); + return; + } + + if (!comment) { + alert('Пожалуйста, укажите комментарий к изменению срока'); + document.getElementById('deadline-change-comment').style.border = '2px solid red'; + document.getElementById('deadline-change-comment').focus(); + return; + } + + // Формируем дату с временем 19:01 по местному времени + const deadlineDateTime = `${newDate}T19:01:00`; + + const submitBtn = document.querySelector('#change-deadline-modal .btn-primary'); + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.innerHTML = '⏳ Сохранение...'; + } + + try { + // Получаем текущие данные задачи + const task = tasks.find(t => t.id === taskId); + + // Обновляем задачу с новой датой + const response = await fetch(`/api/tasks/${taskId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + title: task.title, + description: task.description, + dueDate: deadlineDateTime, + taskType: task.task_type + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Ошибка изменения срока'); + } + + // Добавляем комментарий в лог активности через отдельный эндпоинт + // (если есть API для комментариев) + try { + await fetch(`/api/tasks/${taskId}/comment`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + comment: `📅 Изменение срока: ${comment}`, + type: 'deadline_change' + }) + }); + } catch (commentError) { + console.warn('Не удалось сохранить комментарий:', commentError); + } + + alert('✅ Срок задачи успешно изменен'); + closeChangeDeadlineModal(); + + // Перезагружаем задачи + if (typeof loadTasks === 'function') { + loadTasks(); + } else { + location.reload(); + } + + } catch (error) { + console.error('❌ Ошибка:', error); + alert(`❌ Ошибка: ${error.message}`); + + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.innerHTML = '✅ Изменить срок'; + } + } +} +// ++++++++++++++++++++++++++++++ кнопки изменения срока задачи для исполнителей ++++++++++++++++++++++++++++++ \ No newline at end of file diff --git a/task-endpoints.js b/task-endpoints.js index 8246d09..bf4504b 100644 --- a/task-endpoints.js +++ b/task-endpoints.js @@ -1082,7 +1082,7 @@ app.get('/api/document-approval-tasks', requireAuth, (req, res) => { }); }); }); - +// Обновление всей задачи (включая дату) app.put('/api/tasks/:taskId', requireAuth, upload.array('files', 15), (req, res) => { const { taskId } = req.params; const { title, description, assignedUsers, dueDate } = req.body; diff --git a/test-bd.js b/test-bd.js deleted file mode 100644 index a2f3865..0000000 --- a/test-bd.js +++ /dev/null @@ -1,603 +0,0 @@ -// test-bd.js - Универсальная проверка и исправление структуры базы данных -const sqlite3 = require('sqlite3').verbose(); -const path = require('path'); -const fs = require('fs'); - -// Цвета для вывода в консоль -const colors = { - reset: '\x1b[0m', - red: '\x1b[31m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - magenta: '\x1b[35m', - cyan: '\x1b[36m' -}; - -console.log(`${colors.cyan}🔍 ЗАПУСК ПРОВЕРКИ БАЗЫ ДАННЫХ${colors.reset}`); -console.log('=' .repeat(60)); - -// Путь к базе данных -const dbPath = path.join(__dirname, 'data', 'school_crm.db'); -console.log(`${colors.blue}📁 База данных:${colors.reset} ${dbPath}`); - -// Проверяем существование файла базы данных -if (!fs.existsSync(dbPath)) { - console.log(`${colors.yellow}⚠️ Файл базы данных не найден. Он будет создан при первом запуске.${colors.reset}`); -} else { - const stats = fs.statSync(dbPath); - console.log(`${colors.green}✅ Файл базы данных существует (${(stats.size / 1024).toFixed(2)} KB)${colors.reset}`); -} - -// Подключаемся к базе данных -const db = new sqlite3.Database(dbPath); - -// Определяем ожидаемую структуру всех таблиц -const expectedTables = { - // Основные таблицы пользователей - users: { - columns: [ - { 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: 'avatar', type: 'TEXT' }, - { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, - { name: 'last_login', type: 'DATETIME' }, - { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } - ], - indexes: [ - '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_users_login ON users(login)' - ] - }, - - // Таблица задач - tasks: { - columns: [ - { 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' } - ], - indexes: [ - '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_tasks_original_task_id ON tasks(original_task_id)' - ] - }, - - // Назначения задач - task_assignments: { - columns: [ - { 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' } - ], - indexes: [ - '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 UNIQUE INDEX IF NOT EXISTS idx_task_assignments_unique ON task_assignments(task_id, user_id) WHERE status != "deleted"' - ] - }, - - // Файлы задач - task_files: { - columns: [ - { 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' } - ], - indexes: [ - 'CREATE INDEX IF NOT EXISTS idx_task_files_task_id ON task_files(task_id)', - 'CREATE INDEX IF NOT EXISTS idx_task_files_user_id ON task_files(user_id)' - ] - }, - - // Логи активности - activity_logs: { - columns: [ - { 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' } - ], - indexes: [ - '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)' - ] - }, - - // Группы пользователей - user_groups: { - columns: [ - { 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 0' }, - { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, - { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } - ], - indexes: [ - 'CREATE INDEX IF NOT EXISTS idx_user_groups_name ON user_groups(name)', - 'CREATE INDEX IF NOT EXISTS idx_user_groups_can_approve ON user_groups(can_approve_documents)' - ] - }, - - // Членство в группах - user_group_memberships: { - columns: [ - { 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' } - ], - indexes: [ - '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 UNIQUE INDEX IF NOT EXISTS idx_user_group_memberships_unique ON user_group_memberships(user_id, group_id)' - ] - }, - - // Типы документов (простые) - simple_document_types: { - columns: [ - { 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' } - ], - indexes: [ - 'CREATE INDEX IF NOT EXISTS idx_simple_document_types_name ON simple_document_types(name)' - ] - }, - - // Документы (простые) - simple_documents: { - columns: [ - { 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' } - ], - indexes: [ - '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)' - ] - }, - - // Настройки пользователей - user_settings: { - columns: [ - { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, - { name: 'user_id', type: 'INTEGER UNIQUE NOT NULL' }, - { name: 'email_notifications', type: 'BOOLEAN DEFAULT 1' }, - { name: 'notification_email', type: 'TEXT' }, - { name: 'telegram_notifications', type: 'BOOLEAN DEFAULT 0' }, - { name: 'telegram_chat_id', type: 'TEXT' }, - { name: 'vk_notifications', type: 'BOOLEAN DEFAULT 0' }, - { name: 'vk_user_id', type: 'TEXT' }, - { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, - { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } - ], - indexes: [ - 'CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)' - ] - }, - - // История уведомлений - notification_history: { - columns: [ - { 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' } - ], - indexes: [ - '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 UNIQUE INDEX IF NOT EXISTS idx_notification_history_unique ON notification_history(user_id, task_id, notification_type)' - ] - }, - - // Очередь email - email_queue: { - columns: [ - { 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' } - ], - indexes: [ - '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)' - ] - }, - - // ===== НОВЫЕ ТАБЛИЦЫ ДЛЯ ЧАТА ===== - - // Сообщения чата задач - task_chat_messages: { - columns: [ - { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, - { name: 'task_id', type: 'INTEGER NOT NULL' }, - { name: 'user_id', type: 'INTEGER NOT NULL' }, - { name: 'message', type: 'TEXT NOT NULL' }, - { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, - { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }, - { name: 'is_edited', type: 'BOOLEAN DEFAULT 0' }, - { name: 'is_deleted', type: 'BOOLEAN DEFAULT 0' }, - { name: 'reply_to_id', type: 'INTEGER' } - ], - foreign_keys: [ - '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' - ], - indexes: [ - '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_user_id ON task_chat_messages(user_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_messages_reply_to ON task_chat_messages(reply_to_id)' - ] - }, - - // Файлы в сообщениях чата - task_chat_files: { - columns: [ - { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, - { name: 'message_id', type: 'INTEGER NOT NULL' }, - { name: 'file_path', type: 'TEXT NOT NULL' }, - { name: 'original_name', type: 'TEXT NOT NULL' }, - { name: 'file_size', type: 'INTEGER NOT NULL' }, - { name: 'file_type', type: 'TEXT' }, - { name: 'uploaded_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } - ], - foreign_keys: [ - 'FOREIGN KEY (message_id) REFERENCES task_chat_messages (id) ON DELETE CASCADE' - ], - indexes: [ - 'CREATE INDEX IF NOT EXISTS idx_task_chat_files_message_id ON task_chat_files(message_id)' - ] - }, - - // Прочитанные сообщения - task_chat_reads: { - columns: [ - { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' }, - { name: 'message_id', type: 'INTEGER NOT NULL' }, - { name: 'user_id', type: 'INTEGER NOT NULL' }, - { name: 'read_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' } - ], - foreign_keys: [ - 'FOREIGN KEY (message_id) REFERENCES task_chat_messages (id) ON DELETE CASCADE', - 'FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE' - ], - indexes: [ - '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)', - 'CREATE UNIQUE INDEX IF NOT EXISTS idx_task_chat_reads_unique ON task_chat_reads(message_id, user_id)' - ] - } -}; - -// Функция для получения списка существующих таблиц -function getExistingTables() { - return new Promise((resolve, reject) => { - db.all("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name", [], (err, rows) => { - if (err) { - reject(err); - } else { - resolve(rows.map(row => row.name)); - } - }); - }); -} - -// Функция для получения структуры таблицы -function getTableInfo(tableName) { - return new Promise((resolve, reject) => { - db.all(`PRAGMA table_info(${tableName})`, [], (err, rows) => { - if (err) { - reject(err); - } else { - resolve(rows); - } - }); - }); -} - -// Функция для получения индексов таблицы -function getTableIndexes(tableName) { - return new Promise((resolve, reject) => { - db.all(`PRAGMA index_list(${tableName})`, [], (err, rows) => { - if (err) { - reject(err); - } else { - resolve(rows); - } - }); - }); -} - -// Функция для добавления колонки -function addColumn(tableName, columnName, columnType) { - return new Promise((resolve, reject) => { - console.log(`${colors.yellow} ➕ Добавление колонки: ${columnName} (${columnType})${colors.reset}`); - db.run(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnType}`, function(err) { - if (err) { - console.log(`${colors.red} ❌ Ошибка: ${err.message}${colors.reset}`); - reject(err); - } else { - console.log(`${colors.green} ✅ Колонка добавлена${colors.reset}`); - resolve(); - } - }); - }); -} - -// Функция для создания таблицы -function createTable(tableName, tableDefinition) { - return new Promise((resolve, reject) => { - console.log(`${colors.yellow} 🏗️ Создание таблицы: ${tableName}${colors.reset}`); - - let createSQL = `CREATE TABLE IF NOT EXISTS ${tableName} (\n`; - - // Добавляем колонки - const columnDefs = tableDefinition.columns.map(col => ` ${col.name} ${col.type}`).join(',\n'); - createSQL += columnDefs; - - // Добавляем внешние ключи, если есть - if (tableDefinition.foreign_keys && tableDefinition.foreign_keys.length > 0) { - createSQL += ',\n' + tableDefinition.foreign_keys.map(fk => ` ${fk}`).join(',\n'); - } - - createSQL += '\n)'; - - db.run(createSQL, function(err) { - if (err) { - console.log(`${colors.red} ❌ Ошибка: ${err.message}${colors.reset}`); - reject(err); - } else { - console.log(`${colors.green} ✅ Таблица создана${colors.reset}`); - resolve(); - } - }); - }); -} - -// Функция для создания индекса -function createIndex(indexSQL) { - return new Promise((resolve, reject) => { - db.run(indexSQL, function(err) { - if (err) { - // Игнорируем ошибки создания индексов, так как они могут уже существовать - resolve(); - } else { - resolve(); - } - }); - }); -} - -// Основная функция проверки -async function checkDatabase() { - console.log(`${colors.cyan}🔍 Получение списка существующих таблиц...${colors.reset}`); - - try { - const existingTables = await getExistingTables(); - console.log(`${colors.green}✅ Найдено таблиц: ${existingTables.length}${colors.reset}`); - - // Проверяем наличие всех ожидаемых таблиц - const expectedTableNames = Object.keys(expectedTables); - const missingTables = expectedTableNames.filter(t => !existingTables.includes(t)); - const extraTables = existingTables.filter(t => !expectedTableNames.includes(t) && !t.startsWith('sqlite_')); - - console.log(`\n${colors.cyan}📊 СТАТИСТИКА ТАБЛИЦ:${colors.reset}`); - console.log(` Ожидаемых таблиц: ${expectedTableNames.length}`); - console.log(` Существующих таблиц: ${existingTables.length}`); - console.log(` Отсутствует таблиц: ${missingTables.length}`); - console.log(` Лишних таблиц: ${extraTables.length}`); - - if (extraTables.length > 0) { - console.log(`\n${colors.yellow}⚠️ Лишние таблицы (не требуются, но можно оставить):${colors.reset}`); - extraTables.forEach(t => console.log(` - ${t}`)); - } - - // Проверяем структуру каждой ожидаемой таблицы - console.log(`\n${colors.cyan}🔧 ПРОВЕРКА СТРУКТУРЫ ТАБЛИЦ:${colors.reset}`); - - for (const tableName of expectedTableNames) { - console.log(`\n${colors.magenta}📋 Таблица: ${tableName}${colors.reset}`); - - const tableDef = expectedTables[tableName]; - - if (!existingTables.includes(tableName)) { - // Таблица не существует - создаём - console.log(`${colors.yellow} ⚠️ Таблица не существует${colors.reset}`); - await createTable(tableName, tableDef); - - // Создаём индексы для новой таблицы - if (tableDef.indexes && tableDef.indexes.length > 0) { - console.log(`${colors.yellow} 🔧 Создание индексов...${colors.reset}`); - for (const indexSQL of tableDef.indexes) { - await createIndex(indexSQL); - } - console.log(`${colors.green} ✅ Индексы созданы${colors.reset}`); - } - - continue; - } - - // Таблица существует - проверяем колонки - const columns = await getTableInfo(tableName); - const existingColumnNames = columns.map(c => c.name.toLowerCase()); - - console.log(` 📊 Колонок в БД: ${columns.length}, требуется: ${tableDef.columns.length}`); - - // Проверяем наличие всех необходимых колонок - for (const expectedCol of tableDef.columns) { - const colName = expectedCol.name.toLowerCase(); - - if (!existingColumnNames.includes(colName)) { - console.log(`${colors.yellow} ⚠️ Отсутствует колонка: ${expectedCol.name}${colors.reset}`); - await addColumn(tableName, expectedCol.name, expectedCol.type); - } - } - - // Проверяем типы данных колонок (базовая проверка) - for (const existingCol of columns) { - const expectedCol = tableDef.columns.find(c => c.name.toLowerCase() === existingCol.name.toLowerCase()); - if (expectedCol) { - const expectedType = expectedCol.type.split(' ')[0].toUpperCase(); - const existingType = existingCol.type.toUpperCase(); - - if (!existingType.includes(expectedType) && !expectedType.includes(existingType)) { - console.log(`${colors.yellow} ⚠️ Несоответствие типа: ${existingCol.name} - ожидается ${expectedType}, в БД ${existingType}${colors.reset}`); - console.log(` Ручное изменение типа данных может привести к потере данных. Пропускаем.`); - } - } - } - - // Проверяем индексы - try { - const indexes = await getTableIndexes(tableName); - const existingIndexNames = indexes.map(i => i.name.toLowerCase()); - - if (tableDef.indexes && tableDef.indexes.length > 0) { - console.log(` 🔍 Проверка индексов...`); - - for (const indexSQL of tableDef.indexes) { - // Извлекаем имя индекса из SQL (упрощённо) - const match = indexSQL.match(/INDEX\s+IF NOT EXISTS\s+(\w+)/i) || - indexSQL.match(/INDEX\s+(\w+)/i); - - if (match) { - const indexName = match[1].toLowerCase(); - if (!existingIndexNames.includes(indexName)) { - console.log(`${colors.yellow} ➕ Создание индекса: ${indexName}${colors.reset}`); - await createIndex(indexSQL); - } - } - } - } - } catch (err) { - console.log(`${colors.red} ❌ Ошибка проверки индексов: ${err.message}${colors.reset}`); - } - - // Проверяем внешние ключи (только для SQLite - ограниченная поддержка) - if (tableDef.foreign_keys && tableDef.foreign_keys.length > 0) { - // В SQLite сложно проверить внешние ключи через PRAGMA, просто удостоверимся что таблица создана правильно - console.log(` 🔍 Внешние ключи определены в структуре таблицы`); - } - } - - // Проверяем наличие директорий для файлов - console.log(`\n${colors.cyan}📁 ПРОВЕРКА ДИРЕКТОРИЙ:${colors.reset}`); - - const dirsToCheck = [ - path.join(__dirname, 'data', 'uploads'), - path.join(__dirname, 'data', 'uploads', 'tasks'), - path.join(__dirname, 'data', 'uploads', 'chat'), - path.join(__dirname, 'data', 'logs') - ]; - - dirsToCheck.forEach(dir => { - if (!fs.existsSync(dir)) { - console.log(`${colors.yellow} 📁 Создание директории: ${path.basename(dir)}${colors.reset}`); - fs.mkdirSync(dir, { recursive: true }); - console.log(`${colors.green} ✅ Создано${colors.reset}`); - } else { - console.log(`${colors.green} ✅ Директория существует: ${path.basename(dir)}${colors.reset}`); - } - }); - - // Итоговый отчёт - console.log(`\n${colors.cyan}🏁 ИТОГОВЫЙ ОТЧЁТ:${colors.reset}`); - console.log('=' .repeat(60)); - - // Проверяем все ли таблицы теперь существуют - const finalTables = await getExistingTables(); - const stillMissing = expectedTableNames.filter(t => !finalTables.includes(t)); - - if (stillMissing.length === 0) { - console.log(`${colors.green}✅ Все необходимые таблицы присутствуют в базе данных.${colors.reset}`); - } else { - console.log(`${colors.red}❌ Отсутствуют таблицы: ${stillMissing.join(', ')}${colors.reset}`); - } - - console.log(`\n${colors.green}✨ Проверка базы данных завершена!${colors.reset}`); - console.log('=' .repeat(60)); - - } catch (error) { - console.error(`${colors.red}❌ Критическая ошибка:${colors.reset}`, error); - } finally { - // Закрываем соединение с базой данных - db.close((err) => { - if (err) { - console.error(`${colors.red}❌ Ошибка закрытия БД:${colors.reset}`, err.message); - } else { - console.log(`${colors.green}✅ Соединение с БД закрыто${colors.reset}`); - } - }); - } -} - -// Запускаем проверку -checkDatabase(); \ No newline at end of file