From 9714ac5004d73b6c81d67d07d1be747dfe65d2b9 Mon Sep 17 00:00:00 2001 From: kalugin66 Date: Tue, 27 Jan 2026 15:12:27 +0500 Subject: [PATCH] doc --- database.js | 215 ++++++++++- init-document-types.js | 82 ++++ public/admin-doc.html | 856 +++++++++++++++++++++++++++++++++++++++++ public/doc.html | 433 +++++++++++++++++++++ public/documents.js | 495 ++++++++++++++++++++++++ server.js | 438 ++++++++++++++++++++- 6 files changed, 2499 insertions(+), 20 deletions(-) create mode 100644 init-document-types.js create mode 100644 public/admin-doc.html create mode 100644 public/doc.html create mode 100644 public/documents.js diff --git a/database.js b/database.js index 486d34d..36dfae6 100644 --- a/database.js +++ b/database.js @@ -323,7 +323,32 @@ function createSQLiteTables() { )`); 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('✅ Таблицы для типов документов и документов (расширенные задачи) созданы'); + // Запускаем проверку и обновление структуры таблиц setTimeout(() => { checkAndUpdateTableStructure(); @@ -431,6 +456,23 @@ function checkAndUpdateTableStructure() { { 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' } ] }; @@ -500,7 +542,9 @@ function checkAndUpdateTableStructure() { "CREATE INDEX IF NOT EXISTS idx_tasks_approver_group_id ON tasks(approver_group_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_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)" ]; newIndexes.forEach(indexQuery => { @@ -896,6 +940,31 @@ async function createPostgresTables() { ) `); + // Таблица для типов документов (упрощенная версия) + 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 созданы/проверены'); // Создаем индексы @@ -920,7 +989,10 @@ async function createPostgresTables() { 'CREATE INDEX IF NOT EXISTS idx_documents_status ON documents(status)', 'CREATE INDEX IF NOT EXISTS idx_documents_created_by ON documents(created_by)', 'CREATE INDEX IF NOT EXISTS idx_document_approvals_document_id ON document_approvals(document_id)', - 'CREATE INDEX IF NOT EXISTS idx_document_approvals_status ON document_approvals(status)' + 'CREATE INDEX IF NOT EXISTS idx_document_approvals_status ON document_approvals(status)', + // Индексы для простых документов + '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) { @@ -983,6 +1055,16 @@ async function checkPostgresTableStructure() { { name: 'task_type', type: 'VARCHAR(50) DEFAULT "regular"' }, { name: 'approver_group_id', type: 'INTEGER' }, { name: 'document_id', type: 'INTEGER' } + ], + simple_documents: [ + { name: 'task_id', type: 'INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE' }, + { name: 'document_type_id', type: 'INTEGER REFERENCES simple_document_types(id)' }, + { name: 'document_number', type: 'VARCHAR(100)' }, + { name: 'document_date', type: 'DATE' }, + { name: 'pages_count', type: 'INTEGER' }, + { name: 'urgency_level', type: 'VARCHAR(20) CHECK (urgency_level IN (\'normal\', \'urgent\', \'very_urgent\'))' }, + { name: 'comment', type: 'TEXT' }, + { name: 'refusal_reason', type: 'TEXT' } ] }; @@ -1252,6 +1334,122 @@ function checkOverdueTasks() { 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: () => { @@ -1270,12 +1468,19 @@ module.exports = { USE_POSTGRES, getDatabaseType: () => USE_POSTGRES ? 'PostgreSQL' : 'SQLite', checkAndUpdateTableStructure, // Экспортируем для ручного запуска - // Новые функции для работы с группами + // Функции для работы с группами getUserGroups, getGroupMembers, getApproverGroups, addUserToGroup, - removeUserFromGroup + removeUserFromGroup, + // Функции для работы с простыми документами + createSimpleDocumentType, + getSimpleDocumentTypes, + createSimpleDocument, + getTaskDocuments, + updateSimpleDocument, + deleteSimpleDocument }; // Запускаем инициализацию при экспорте (но она завершится позже) diff --git a/init-document-types.js b/init-document-types.js new file mode 100644 index 0000000..6c4044e --- /dev/null +++ b/init-document-types.js @@ -0,0 +1,82 @@ +// init-document-types.js - Инициализация типов документов +const fs = require('fs'); +const path = require('path'); + +function initializeDocumentTypes(db) { + const documentTypes = [ + { name: 'Приказ', description: 'Распорядительный документ' }, + { name: 'Распоряжение', description: 'Распорядительный документ' }, + { name: 'Инструкция', description: 'Нормативный документ' }, + { name: 'Положение', description: 'Нормативный документ' }, + { name: 'Договор', description: 'Юридический документ' }, + { name: 'Соглашение', description: 'Юридический документ' }, + { name: 'Акт', description: 'Документ подтверждения факта' }, + { name: 'Служебная записка', description: 'Внутренний документ' }, + { name: 'Заявление', description: 'Обращение' }, + { name: 'Отчет', description: 'Отчетный документ' }, + { name: 'План', description: 'Плановый документ' }, + { name: 'Программа', description: 'Плановый документ' }, + { name: 'Протокол', description: 'Документ собрания' }, + { name: 'Решение', description: 'Документ коллегиального органа' }, + { name: 'Письмо', description: 'Корреспонденция' }, + { name: 'Справка', description: 'Информационный документ' }, + { name: 'Выписка', description: 'Копия части документа' }, + { name: 'Копия', description: 'Копия документа' }, + { name: 'Проект', description: 'Проект документа' }, + { name: 'Шаблон', description: 'Шаблон документа' } + ]; + + // Создаем таблицу если её нет + db.run(` + CREATE TABLE IF NOT EXISTS document_types ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `, (err) => { + if (err) { + console.error('❌ Ошибка создания таблицы типов документов:', err); + return; + } + + console.log('✅ Таблица типов документов создана/проверена'); + + // Проверяем, есть ли уже типы документов + db.get("SELECT COUNT(*) as count FROM document_types", [], (err, result) => { + if (err) { + console.error('❌ Ошибка проверки типов документов:', err); + return; + } + + if (result.count === 0) { + console.log('📄 Добавление типов документов...'); + + const insertPromises = documentTypes.map(type => { + return new Promise((resolve, reject) => { + db.run( + "INSERT INTO document_types (name, description) VALUES (?, ?)", + [type.name, type.description], + (err) => { + if (err) reject(err); + else resolve(); + } + ); + }); + }); + + Promise.all(insertPromises) + .then(() => { + console.log(`✅ Добавлено ${documentTypes.length} типов документов`); + }) + .catch(error => { + console.error('❌ Ошибка добавления типов документов:', error); + }); + } else { + console.log(`✅ В базе уже есть ${result.count} типов документов`); + } + }); + }); +} + +module.exports = { initializeDocumentTypes }; \ No newline at end of file diff --git a/public/admin-doc.html b/public/admin-doc.html new file mode 100644 index 0000000..25d8174 --- /dev/null +++ b/public/admin-doc.html @@ -0,0 +1,856 @@ + + + + + + + Управление группами пользователей | CRM + + + + + +
+
+

Управление группами пользователей

+
+ + +
+
+ +
+ + + +
+ + +
+
+
+
+

Группа "Секретарь"

+

Пользователи этой группы могут согласовывать документы в системе

+
+
+ + + +
+
+
0
+
Всего пользователей
+
+
+
0
+
В группе "Секретарь"
+
+
+ +
+
+ +

Загрузка пользователей...

+
+
+
+ + +
+
+
+
+

Группа "Администрация"

+

Пользователи этой группы имеют права администратора в системе

+
+
+ + + +
+
+
0
+
Всего пользователей
+
+
+
0
+
В группе "Администрация"
+
+
+ +
+
+ +

Загрузка пользователей...

+
+
+
+ + +
+ + +
+
+
0
+
Всего пользователей
+
+
+
0
+
Администраторы
+
+
+
0
+
Секретари
+
+
+ +
+
+ +

Загрузка пользователей...

+
+
+
+
+ + + + \ No newline at end of file diff --git a/public/doc.html b/public/doc.html new file mode 100644 index 0000000..f37b124 --- /dev/null +++ b/public/doc.html @@ -0,0 +1,433 @@ + + + + + + Согласование документов - School CRM + + + + + + + +
+
+
+

Согласование документов

+ +
+ + +
+ +
+ +
+

Создание документа для согласования

+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+
+ + +
+
+ + +
+

Мои документы на согласование

+
+ +
+
+ + +
+

Документы для согласования

+
+ +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/documents.js b/public/documents.js new file mode 100644 index 0000000..a538f17 --- /dev/null +++ b/public/documents.js @@ -0,0 +1,495 @@ +// documents.js - Работа с документами для согласования + +let documentTypes = []; + +async function loadDocumentTypes() { + try { + const response = await fetch('/api/document-types'); + documentTypes = await response.json(); + populateDocumentTypeSelect(); + } catch (error) { + console.error('Ошибка загрузки типов документов:', error); + } +} + +function populateDocumentTypeSelect() { + const select = document.getElementById('document-type'); + if (!select) return; + + select.innerHTML = ''; + + documentTypes.forEach(type => { + const option = document.createElement('option'); + option.value = type.id; + option.textContent = type.name; + select.appendChild(option); + }); +} + +function initializeDocumentForm() { + const form = document.getElementById('create-document-form'); + if (form) { + form.addEventListener('submit', createDocumentTask); + } + + // Инициализация даты по умолчанию + const today = new Date(); + const todayStr = today.toISOString().split('T')[0]; + const dateInput = document.getElementById('document-date'); + if (dateInput) { + dateInput.value = todayStr; + } + + loadDocumentTypes(); +} + +async function createDocumentTask(event) { + event.preventDefault(); + + if (!currentUser) { + alert('Требуется аутентификация'); + return; + } + + const formData = new FormData(); + + // Основные данные задачи + formData.append('title', document.getElementById('document-title').value); + formData.append('description', document.getElementById('document-description').value); + + // Даты + const dueDateInput = document.getElementById('due-date'); + if (dueDateInput.value) { + formData.append('dueDate', dueDateInput.value); + } + + // Данные документа + formData.append('documentTypeId', document.getElementById('document-type').value); + formData.append('documentNumber', document.getElementById('document-number').value); + formData.append('documentDate', document.getElementById('document-date').value); + formData.append('pagesCount', document.getElementById('pages-count').value); + formData.append('urgencyLevel', document.getElementById('urgency-level').value); + formData.append('comment', document.getElementById('document-comment').value); + + // Загружаем файлы + const filesInput = document.getElementById('document-files'); + if (filesInput.files) { + for (let i = 0; i < filesInput.files.length; i++) { + formData.append('files', filesInput.files[i]); + } + } + + try { + const response = await fetch('/api/documents', { + method: 'POST', + body: formData + }); + + if (response.ok) { + alert('Задача на согласование документа создана!'); + document.getElementById('create-document-form').reset(); + document.getElementById('document-file-list').innerHTML = ''; + + // Сброс даты + const today = new Date(); + const todayStr = today.toISOString().split('T')[0]; + const dateInput = document.getElementById('document-date'); + if (dateInput) { + dateInput.value = todayStr; + } + + // Перенаправление на список документов + showDocumentSection('my-documents'); + } else { + const error = await response.json(); + alert(error.error || 'Ошибка создания задачи'); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Ошибка создания задачи'); + } +} + +function updateDocumentFileList() { + const fileInput = document.getElementById('document-files'); + const fileList = document.getElementById('document-file-list'); + + const files = fileInput.files; + if (files.length === 0) { + fileList.innerHTML = ''; + return; + } + + let html = ''; + html += `

Общий размер: ${(totalSize / 1024 / 1024).toFixed(2)} MB / 300 MB

`; + + fileList.innerHTML = html; +} + +// Функции для работы с документами +async function loadMyDocuments() { + try { + const response = await fetch('/api/documents/my'); + const documents = await response.json(); + renderMyDocuments(documents); + } catch (error) { + console.error('Ошибка загрузки документов:', error); + } +} + +async function loadSecretaryDocuments() { + try { + const response = await fetch('/api/documents/secretary'); + const documents = await response.json(); + renderSecretaryDocuments(documents); + } catch (error) { + console.error('Ошибка загрузки документов секретаря:', error); + } +} + +function renderMyDocuments(documents) { + const container = document.getElementById('my-documents-list'); + if (!container) return; + + if (documents.length === 0) { + container.innerHTML = '
У вас нет документов на согласование
'; + return; + } + + container.innerHTML = documents.map(doc => ` +
+
+
+ Документ №${doc.document_number || doc.id} + ${doc.title} + ${getDocumentStatusText(doc.status)} + ${doc.urgency_level === 'urgent' ? 'Срочно' : ''} + ${doc.urgency_level === 'very_urgent' ? 'Очень срочно' : ''} +
+
+ Создан: ${formatDateTime(doc.created_at)} + ${doc.due_date ? `Срок: ${formatDateTime(doc.due_date)}` : ''} +
+
+ +
+
+

Тип: ${doc.document_type_name || 'Не указан'}

+

Дата документа: ${doc.document_date ? formatDate(doc.document_date) : 'Не указана'}

+

Количество страниц: ${doc.pages_count || 'Не указано'}

+ ${doc.comment ? `

Комментарий: ${doc.comment}

` : ''} +
+ +
+ ${doc.files && doc.files.length > 0 ? ` + Файлы: +
+ ${doc.files.map(file => renderFileIcon(file)).join('')} +
+ ` : 'Файлы: нет файлов'} +
+ + ${doc.refusal_reason ? ` +
+ Причина отказа: ${doc.refusal_reason} +
+ ` : ''} + +
+ ${doc.status === 'assigned' || doc.status === 'in_progress' ? ` + + ` : ''} + + ${doc.status === 'refused' ? ` + + ` : ''} + + ${doc.status === 'approved' || doc.status === 'received' || doc.status === 'signed' ? ` + + ` : ''} +
+
+
+ `).join(''); +} + +function renderSecretaryDocuments(documents) { + const container = document.getElementById('secretary-documents-list'); + if (!container) return; + + if (documents.length === 0) { + container.innerHTML = '
Нет документов для согласования
'; + return; + } + + container.innerHTML = documents.map(doc => ` +
+
+
+ Документ №${doc.document_number || doc.id} + ${doc.title} + ${getDocumentStatusText(doc.status)} + ${doc.urgency_level === 'urgent' ? 'Срочно' : ''} + ${doc.urgency_level === 'very_urgent' ? 'Очень срочно' : ''} +
+
+ От: ${doc.creator_name} + Создан: ${formatDateTime(doc.created_at)} + ${doc.due_date ? `Срок: ${formatDateTime(doc.due_date)}` : ''} +
+
+ +
+
+

Тип: ${doc.document_type_name || 'Не указан'}

+

Номер: ${doc.document_number || 'Не указан'}

+

Дата документа: ${doc.document_date ? formatDate(doc.document_date) : 'Не указана'}

+

Количество страниц: ${doc.pages_count || 'Не указано'}

+ ${doc.comment ? `

Комментарий автора: ${doc.comment}

` : ''} +
+ +
+ ${doc.files && doc.files.length > 0 ? ` + Файлы: +
+ ${doc.files.map(file => renderFileIcon(file)).join('')} +
+ ` : 'Файлы: нет файлов'} +
+ +
+ ${doc.status === 'assigned' ? ` + + ` : ''} + + ${doc.status === 'in_progress' ? ` +
+ + + +
+ ` : ''} + + ${doc.status === 'approved' ? ` +
+ + +
+ ` : ''} + + ${doc.status === 'received' ? ` +
+ + +
+ ` : ''} + + ${doc.status === 'refused' ? ` +

Причина отказа: ${doc.refusal_reason}

+ ` : ''} +
+
+
+ `).join(''); +} + +function getDocumentStatusClass(status) { + switch(status) { + case 'assigned': return 'status-assigned'; + case 'in_progress': return 'status-in-progress'; + case 'approved': return 'status-approved'; + case 'received': return 'status-received'; + case 'signed': return 'status-signed'; + case 'refused': return 'status-refused'; + case 'cancelled': return 'status-cancelled'; + default: return 'status-assigned'; + } +} + +function getDocumentStatusText(status) { + switch(status) { + case 'assigned': return 'Назначена'; + case 'in_progress': return 'В работе'; + case 'approved': return 'Согласован'; + case 'received': return 'Получен'; + case 'signed': return 'Подписан'; + case 'refused': return 'Отказано'; + case 'cancelled': return 'Отозвано'; + default: return status; + } +} + +function formatDate(dateString) { + if (!dateString) return ''; + return new Date(dateString).toLocaleDateString('ru-RU'); +} + +// Модальные окна для секретаря +function showApproveModal(documentId) { + currentDocumentId = documentId; + document.getElementById('approve-document-modal').style.display = 'block'; +} + +function closeApproveModal() { + document.getElementById('approve-document-modal').style.display = 'none'; + document.getElementById('approve-comment').value = ''; +} + +function showReceiveModal(documentId) { + currentDocumentId = documentId; + document.getElementById('receive-document-modal').style.display = 'block'; +} + +function closeReceiveModal() { + document.getElementById('receive-document-modal').style.display = 'none'; + document.getElementById('receive-comment').value = ''; +} + +function showRefuseModal(documentId) { + currentDocumentId = documentId; + document.getElementById('refuse-document-modal').style.display = 'block'; +} + +function closeRefuseModal() { + document.getElementById('refuse-document-modal').style.display = 'none'; + document.getElementById('refuse-reason').value = ''; +} + +let currentDocumentId = null; + +// Функции для работы с API +async function updateDocumentStatus(documentId, status, comment = '', refusalReason = '') { + try { + const response = await fetch(`/api/documents/${documentId}/status`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + status: status, + comment: comment, + refusalReason: refusalReason + }) + }); + + if (response.ok) { + alert('Статус документа обновлен!'); + + // Закрываем модальные окна + closeApproveModal(); + closeReceiveModal(); + closeRefuseModal(); + + // Обновляем список документов + if (isSecretary()) { + loadSecretaryDocuments(); + } + loadMyDocuments(); + } else { + const error = await response.json(); + alert(error.error || 'Ошибка обновления статуса'); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Ошибка обновления статуса'); + } +} + +async function cancelDocument(documentId) { + if (!confirm('Вы уверены, что хотите отозвать документ?')) { + return; + } + + try { + const response = await fetch(`/api/documents/${documentId}/cancel`, { + method: 'POST' + }); + + if (response.ok) { + alert('Документ отозван!'); + loadMyDocuments(); + } else { + const error = await response.json(); + alert(error.error || 'Ошибка отзыва документа'); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Ошибка отзыва документа'); + } +} + +async function reworkDocument(documentId) { + // Здесь можно открыть форму для повторной отправки + alert('Функция исправления и повторной отправки будет реализована в следующей версии'); +} + +async function downloadDocumentPackage(documentId) { + try { + const response = await fetch(`/api/documents/${documentId}/package`); + if (response.ok) { + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `document_${documentId}_package.zip`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } else { + const error = await response.json(); + alert(error.error || 'Ошибка скачивания пакета документов'); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Ошибка скачивания пакета документов'); + } +} + +function isSecretary() { + return currentUser && currentUser.groups && currentUser.groups.includes('Секретарь'); +} + +function showDocumentSection(sectionName) { + // Скрываем все секции + document.querySelectorAll('.document-section').forEach(section => { + section.style.display = 'none'; + }); + + // Показываем выбранную секцию + const targetSection = document.getElementById(`${sectionName}-section`); + if (targetSection) { + targetSection.style.display = 'block'; + } + + // Загружаем данные для секции + if (sectionName === 'my-documents') { + loadMyDocuments(); + } else if (sectionName === 'secretary-documents' && isSecretary()) { + loadSecretaryDocuments(); + } +} + +// Инициализация при загрузке страницы +document.addEventListener('DOMContentLoaded', function() { + if (window.location.pathname === '/doc') { + initializeDocumentForm(); + + // Показываем соответствующие секции + if (isSecretary()) { + document.getElementById('secretary-tab').style.display = 'block'; + } + + // По умолчанию показываем создание документа + showDocumentSection('create-document'); + } +}); \ No newline at end of file diff --git a/server.js b/server.js index d9d360b..b593b3b 100644 --- a/server.js +++ b/server.js @@ -14,7 +14,6 @@ const { sendTaskNotifications, checkUpcomingDeadlines, getStatusText } = require const { setupUploadMiddleware } = require('./upload-middleware'); const { setupTaskEndpoints } = require('./task-endpoints'); - const app = express(); const PORT = process.env.PORT || 3000; @@ -24,6 +23,37 @@ let serverReady = false; let adminRouter = null; let upload = null; +// Инициализируем multer сразу с настройками по умолчанию +const uploadsDir = path.join(__dirname, 'data', 'uploads'); +const createDirIfNotExists = (dirPath) => { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } +}; +createDirIfNotExists(uploadsDir); + +// Создаем базовую конфигурацию multer +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, uploadsDir); + }, + filename: function (req, file, cb) { + // Используем оригинальное имя файла с timestamp для уникальности + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const ext = path.extname(file.originalname); + const name = path.basename(file.originalname, ext); + cb(null, name + '-' + uniqueSuffix + ext); + } +}); + +// Создаем экземпляр multer сразу +upload = multer({ + storage: storage, + limits: { + fileSize: 50 * 1024 * 1024 // 50MB + } +}); + // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); @@ -40,7 +70,6 @@ app.use(session({ } })); - // Middleware для проверки готовности сервера app.use((req, res, next) => { if (!serverReady && req.path !== '/health' && req.path !== '/api/health') { @@ -72,12 +101,6 @@ app.get('/api/health', (req, res) => { }); // Вспомогательные функции -const createDirIfNotExists = (dirPath) => { - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - } -}; - function checkIfOverdue(dueDate, status) { if (!dueDate || status === 'completed') return false; const now = new Date(); @@ -552,6 +575,7 @@ app.get('/admin', (req, res) => { } res.sendFile(path.join(__dirname, 'public/admin.html')); }); + // Страница профилей пользователей (только для админов) app.get('/admin/profiles', (req, res) => { if (!req.session.user || req.session.user.role !== 'admin') { @@ -559,6 +583,7 @@ app.get('/admin/profiles', (req, res) => { } res.sendFile(path.join(__dirname, 'public/admin-profiles.html')); }); + // Админ панель для документов app.get('/admin-doc', (req, res) => { if (!req.session.user || req.session.user.role !== 'admin') { @@ -575,6 +600,387 @@ app.get('/doc', (req, res) => { res.sendFile(path.join(__dirname, 'public/doc.html')); }); +// API для типов документов +app.get('/api/document-types', requireAuth, (req, res) => { + db.all("SELECT * FROM document_types ORDER BY name", [], (err, rows) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + res.json(rows); + }); +}); + +// API для документов (ИСПРАВЛЕНО: upload определен в начале файла) +app.post('/api/documents', requireAuth, upload.array('files', 15), async (req, res) => { + try { + const userId = req.session.user.id; + const { + title, + description, + dueDate, + documentTypeId, + documentNumber, + documentDate, + pagesCount, + urgencyLevel, + comment + } = req.body; + + // Находим группу "Секретарь" + db.get("SELECT id FROM users WHERE groups LIKE '%Секретарь%' OR groups LIKE '%\"Секретарь\"%' LIMIT 1", async (err, secretary) => { + if (err) { + return res.status(500).json({ error: err.message }); + } + + if (!secretary) { + return res.status(400).json({ error: 'Не найден секретарь для согласования документов' }); + } + + // Создаем задачу + db.run(` + INSERT INTO tasks (title, description, due_date, created_by, status) + VALUES (?, ?, ?, ?, 'assigned') + `, [title, description, dueDate || null, userId], function(err) { + if (err) { + return res.status(500).json({ error: err.message }); + } + + const taskId = this.lastID; + + // Создаем запись документа + db.run(` + INSERT INTO documents ( + task_id, document_type_id, document_number, + document_date, pages_count, urgency_level, comment + ) VALUES (?, ?, ?, ?, ?, ?, ?) + `, [ + taskId, + documentTypeId || null, + documentNumber || null, + documentDate || null, + pagesCount || null, + urgencyLevel || 'normal', + comment || null + ], function(err) { + if (err) { + // Удаляем задачу если не удалось создать документ + db.run("DELETE FROM tasks WHERE id = ?", [taskId]); + return res.status(500).json({ error: err.message }); + } + + // Назначаем задачу секретарю + db.run(` + INSERT INTO task_assignments (task_id, user_id, status) + VALUES (?, ?, 'assigned') + `, [taskId, secretary.id], function(err) { + if (err) { + return res.status(500).json({ error: err.message }); + } + + // Загружаем файлы если есть + if (req.files && req.files.length > 0) { + const uploadPromises = req.files.map(file => { + return new Promise((resolve, reject) => { + const filePath = file.path; + const originalName = Buffer.from(file.originalname, 'latin1').toString('utf8'); + + db.run(` + INSERT INTO task_files (task_id, user_id, file_path, original_name, file_size) + VALUES (?, ?, ?, ?, ?) + `, [taskId, userId, filePath, originalName, file.size], function(err) { + if (err) reject(err); + else resolve(); + }); + }); + }); + + Promise.all(uploadPromises) + .then(() => { + const { logActivity } = require('./database'); + logActivity(taskId, userId, 'TASK_CREATED', `Создан документ для согласования: ${title}`); + res.json({ success: true, taskId: taskId }); + }) + .catch(error => { + console.error('Ошибка загрузки файлов:', error); + res.json({ success: true, taskId: taskId }); + }); + } else { + const { logActivity } = require('./database'); + logActivity(taskId, userId, 'TASK_CREATED', `Создан документ для согласования: ${title}`); + res.json({ success: true, taskId: taskId }); + } + }); + }); + }); + }); + } catch (error) { + console.error('Ошибка создания документа:', error); + res.status(500).json({ error: 'Ошибка создания документа' }); + } +}); + +// Получение моих документов +app.get('/api/documents/my', requireAuth, (req, res) => { + const userId = req.session.user.id; + + db.all(` + SELECT + t.id, + t.title, + t.description, + t.due_date, + t.created_at, + t.status, + d.document_type_id, + dt.name as document_type_name, + d.document_number, + d.document_date, + d.pages_count, + d.urgency_level, + d.comment, + d.refusal_reason, + u.name as creator_name, + GROUP_CONCAT(tf.id) as file_ids + FROM tasks t + LEFT JOIN documents d ON t.id = d.task_id + LEFT JOIN document_types dt ON d.document_type_id = dt.id + LEFT JOIN users u ON t.created_by = u.id + LEFT JOIN task_files tf ON t.id = tf.task_id + WHERE t.created_by = ? + AND t.title LIKE 'Документ:%' + GROUP BY t.id + ORDER BY t.created_at DESC + `, [userId], async (err, tasks) => { + if (err) { + return res.status(500).json({ error: err.message }); + } + + // Загружаем файлы для каждой задачи + const tasksWithFiles = await Promise.all(tasks.map(async (task) => { + if (task.file_ids) { + const fileIds = task.file_ids.split(',').filter(id => id); + const files = await new Promise((resolve, reject) => { + db.all(` + SELECT tf.*, u.name as user_name + FROM task_files tf + LEFT JOIN users u ON tf.user_id = u.id + WHERE tf.id IN (${fileIds.map(() => '?').join(',')}) + `, fileIds, (err, rows) => { + if (err) reject(err); + else resolve(rows); + }); + }); + task.files = files; + } else { + task.files = []; + } + return task; + })); + + res.json(tasksWithFiles); + }); +}); + +// Получение документов для секретаря +app.get('/api/documents/secretary', requireAuth, (req, res) => { + const userId = req.session.user.id; + + // Проверяем, что пользователь секретарь + if (!req.session.user.groups || !req.session.user.groups.includes('Секретарь')) { + return res.status(403).json({ error: 'Недостаточно прав' }); + } + + db.all(` + SELECT + t.id, + t.title, + t.description, + t.due_date, + t.created_at, + ta.status, + d.document_type_id, + dt.name as document_type_name, + d.document_number, + d.document_date, + d.pages_count, + d.urgency_level, + d.comment, + d.refusal_reason, + u.name as creator_name, + GROUP_CONCAT(tf.id) as file_ids + FROM tasks t + JOIN task_assignments ta ON t.id = ta.task_id + LEFT JOIN documents d ON t.id = d.task_id + LEFT JOIN document_types dt ON d.document_type_id = dt.id + LEFT JOIN users u ON t.created_by = u.id + LEFT JOIN task_files tf ON t.id = tf.task_id + WHERE ta.user_id = ? + AND t.title LIKE 'Документ:%' + AND t.status = 'active' + AND t.closed_at IS NULL + GROUP BY t.id + ORDER BY + CASE d.urgency_level + WHEN 'very_urgent' THEN 1 + WHEN 'urgent' THEN 2 + ELSE 3 + END, + t.due_date ASC, + t.created_at DESC + `, [userId], async (err, tasks) => { + if (err) { + return res.status(500).json({ error: err.message }); + } + + // Загружаем файлы для каждой задачи + const tasksWithFiles = await Promise.all(tasks.map(async (task) => { + if (task.file_ids) { + const fileIds = task.file_ids.split(',').filter(id => id); + const files = await new Promise((resolve, reject) => { + db.all(` + SELECT tf.*, u.name as user_name + FROM task_files tf + LEFT JOIN users u ON tf.user_id = u.id + WHERE tf.id IN (${fileIds.map(() => '?').join(',')}) + `, fileIds, (err, rows) => { + if (err) reject(err); + else resolve(rows); + }); + }); + task.files = files; + } else { + task.files = []; + } + return task; + })); + + res.json(tasksWithFiles); + }); +}); + +// Обновление статуса документа +app.put('/api/documents/:id/status', requireAuth, (req, res) => { + const documentId = req.params.id; + const { status, comment, refusalReason } = req.body; + const userId = req.session.user.id; + + // Проверяем права (только секретарь или администратор) + if (!req.session.user.groups || !req.session.user.groups.includes('Секретарь')) { + if (req.session.user.role !== 'admin') { + return res.status(403).json({ error: 'Недостаточно прав' }); + } + } + + db.get("SELECT task_id FROM documents WHERE id = ?", [documentId], (err, document) => { + if (err || !document) { + return res.status(404).json({ error: 'Документ не найден' }); + } + + const taskId = document.task_id; + + // Обновляем статус в задании + db.run("UPDATE task_assignments SET status = ? WHERE task_id = ? AND user_id = ?", + [status, taskId, userId], function(err) { + if (err) { + return res.status(500).json({ error: err.message }); + } + + // Обновляем причину отказа если есть + if (refusalReason) { + db.run("UPDATE documents SET refusal_reason = ? WHERE id = ?", + [refusalReason, documentId]); + } + + // Логируем действие + const { logActivity } = require('./database'); + const actionMap = { + 'approved': 'Документ согласован', + 'received': 'Оригинал документа получен', + 'signed': 'Документ подписан', + 'refused': 'В согласовании отказано' + }; + + const actionText = actionMap[status] || `Статус изменен на: ${status}`; + logActivity(taskId, userId, 'STATUS_CHANGED', actionText); + + res.json({ success: true }); + } + ); + }); +}); + +// Отзыв документа +app.post('/api/documents/:id/cancel', requireAuth, (req, res) => { + const documentId = req.params.id; + const userId = req.session.user.id; + + db.get("SELECT task_id FROM documents WHERE id = ?", [documentId], (err, document) => { + if (err || !document) { + return res.status(404).json({ error: 'Документ не найден' }); + } + + const taskId = document.task_id; + + // Проверяем, что пользователь создатель задачи + db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => { + if (err || !task) { + return res.status(404).json({ error: 'Задача не найдена' }); + } + + if (parseInt(task.created_by) !== parseInt(userId)) { + return res.status(403).json({ error: 'Вы не являетесь создателем этого документа' }); + } + + // Обновляем статус задачи + db.run("UPDATE tasks SET status = 'cancelled' WHERE id = ?", [taskId], function(err) { + if (err) { + return res.status(500).json({ error: err.message }); + } + + // Логируем действие + const { logActivity } = require('./database'); + logActivity(taskId, userId, 'STATUS_CHANGED', 'Документ отозван создателем'); + + res.json({ success: true }); + }); + }); + }); +}); + +// Получение пакета документов +app.get('/api/documents/:id/package', requireAuth, async (req, res) => { + const documentId = req.params.id; + const userId = req.session.user.id; + + // Проверяем доступ к документу + db.get(` + SELECT t.id, t.created_by + FROM documents d + JOIN tasks t ON d.task_id = t.id + WHERE d.id = ? + `, [documentId], async (err, result) => { + if (err || !result) { + return res.status(404).json({ error: 'Документ не найден' }); + } + + // Проверяем, что пользователь имеет доступ (создатель или секретарь) + const isCreator = parseInt(result.created_by) === parseInt(userId); + const isSecretary = req.session.user.groups && req.session.user.groups.includes('Секретарь'); + + if (!isCreator && !isSecretary) { + return res.status(403).json({ error: 'Недостаточно прав' }); + } + + // Здесь будет логика создания ZIP архива с документами + // Пока возвращаем заглушку + res.json({ + success: true, + message: 'Функция создания пакета документов будет реализована в следующей версии' + }); + }); +}); + // API для получения настроек уведомлений пользователя app.get('/api/user/settings', requireAuth, async (req, res) => { try { @@ -782,6 +1188,7 @@ app.get('/api/email-health', requireAuth, async (req, res) => { res.status(500).json({ error: error.message }); } }); + // Страница управления группами app.get('/admin/groups', (req, res) => { if (!req.session.user || req.session.user.role !== 'admin') { @@ -789,6 +1196,7 @@ app.get('/admin/groups', (req, res) => { } res.sendFile(path.join(__dirname, 'public/admin-groups.html')); }); + // Инициализация сервера async function initializeServer() { console.log('🚀 Инициализация сервера...'); @@ -801,20 +1209,20 @@ async function initializeServer() { // 2. Получаем объект БД db = getDb(); console.log('✅ База данных готова'); + + const { initializeDocumentTypes } = require('./init-document-types'); + initializeDocumentTypes(db); + console.log('✅ Сервис document готов'); // 3. Настраиваем authService с БД authService.setDatabase(db); console.log('✅ Сервис аутентификации готов'); - // 4. Настраиваем загрузку файлов - upload = setupUploadMiddleware(); - console.log('✅ Middleware загрузки файлов настроен'); - - // 5. Настраиваем endpoint'ы для задач + // 4. Настраиваем endpoint'ы для задач (upload уже настроен в начале файла) setupTaskEndpoints(app, db, upload); console.log('✅ Endpoint\'ы задач настроены'); - // 6. Загружаем админ роутер динамически + // 5. Загружаем админ роутер динамически try { adminRouter = require('./admin-server'); console.log('Admin router loaded:', adminRouter); @@ -851,7 +1259,7 @@ async function initializeServer() { console.log('⚠️ Создана заглушка для админ роутера из-за ошибки'); } - // 7. Помечаем сервер как готовый + // 6. Помечаем сервер как готовый serverReady = true; console.log('✅ Сервер полностью инициализирован');