Изменить срок

This commit is contained in:
2026-02-25 10:26:45 +05:00
parent fd928b6e3a
commit 6820550cbd
4 changed files with 283 additions and 607 deletions

View File

@@ -4654,4 +4654,110 @@ button.btn-primary {
word-break: break-word; word-break: break-word;
max-width: 100%; max-width: 100%;
} }
} }
/* Стили для кнопки изменения срока */
.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;
}
}

View File

@@ -212,7 +212,11 @@ ${currentUser && currentUser.role === 'tasks' && canEdit || currentUser.role ===
${currentUser && currentUser.login === 'kalugin.o' ? `<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Вернуть на доработку">🔄</button>` : ''} ${currentUser && currentUser.login === 'kalugin.o' ? `<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Вернуть на доработку">🔄</button>` : ''}
<!-- Кнопка переделки документа для исполнителей --> <!-- Кнопка переделки документа для исполнителей -->
${task.task_type === 'document' && userRole === 'Исполнитель' ? `<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Переделать документ">🔄Переделать</button>` : ''} ${task.task_type === 'document' && userRole === 'Исполнитель' ? `<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Переделать документ">🔄Переделать</button>` : ''}
${currentUser && currentUser.login === 'minicrm' ? `<button class="close-btn" onclick="closeTask(${task.id})" title="Закрыть задачу">🔒</button>` : ''}
${!isDeleted && !isClosed && task.task_type !== 'regular' && task.assignments && task.assignments.some(a => parseInt(a.user_id) === currentUser.id) ?
`<button class="change-deadline-btn" onclick="openChangeDeadlineModal(${task.id})" title="Изменить срок">📅</button>` : ''}
${currentUser && currentUser.login === 'minicrm' ? `<button class="close-btn" onclick="closeTask(${task.id})" title="Закрыть задачу">🔒</button>` : ''}
${canEdit ? `<button class="delete-btn" onclick="deleteTask(${task.id})" title="Удалить">🗑️</button>` : ''} ${canEdit ? `<button class="delete-btn" onclick="deleteTask(${task.id})" title="Удалить">🗑️</button>` : ''}
` : ''} ` : ''}
${isClosed && canEdit ? ` ${isClosed && canEdit ? `
@@ -1927,4 +1931,173 @@ function renderGroupedFiles(task) {
</div> </div>
</div> </div>
`).join(''); `).join('');
} }
// ++++++++++++++++++++++++++++++ кнопки изменения срока задачи для исполнителей ++++++++++++++++++++++++++++++
// Функция для открытия модального окна изменения срока задачи
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 = `
<div class="modal" id="change-deadline-modal">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3>📅 Изменение срока задачи</h3>
<span class="close" onclick="closeChangeDeadlineModal()">&times;</span>
</div>
<div class="modal-body">
<p><strong>Задача:</strong> ${escapeHtml(task.title)}</p>
<p><strong>Тип:</strong> ${getTaskTypeDisplayName(task.task_type)}</p>
<p><strong>Текущий срок:</strong> ${formatDateTime(task.due_date)}</p>
<div class="form-group">
<label for="new-deadline-date"><strong>Новая дата выполнения:</strong></label>
<input type="date"
id="new-deadline-date"
class="form-control"
value="${new Date().toISOString().split('T')[0]}"
min="${new Date().toISOString().split('T')[0]}">
</div>
<div class="form-group">
<label for="deadline-change-comment"><strong>Комментарий к изменению срока:</strong></label>
<textarea id="deadline-change-comment"
class="form-control"
rows="4"
placeholder="Укажите причину изменения срока..."
required></textarea>
</div>
<div class="modal-footer">
<button type="button" class="nav-btn admin" onclick="closeChangeDeadlineModal()">Отмена</button>
<button type="button" class="btn-primary" onclick="submitDeadlineChange(${taskId})">✅ Изменить срок</button>
</div>
</div>
</div>
</div>
`;
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 = '✅ Изменить срок';
}
}
}
// ++++++++++++++++++++++++++++++ кнопки изменения срока задачи для исполнителей ++++++++++++++++++++++++++++++

View File

@@ -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) => { app.put('/api/tasks/:taskId', requireAuth, upload.array('files', 15), (req, res) => {
const { taskId } = req.params; const { taskId } = req.params;
const { title, description, assignedUsers, dueDate } = req.body; const { title, description, assignedUsers, dueDate } = req.body;

View File

@@ -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();