hz
This commit is contained in:
1279
admin-server.js
Normal file
1279
admin-server.js
Normal file
File diff suppressed because it is too large
Load Diff
381
database.js
381
database.js
@@ -195,17 +195,17 @@ function createSQLiteTables() {
|
||||
console.log('✅ Таблица для пользовательских настроек инициализирована');
|
||||
|
||||
// Таблица для типов документов
|
||||
db.run(`CREATE TABLE IF NOT EXISTS document_types (
|
||||
db.run(`CREATE TABLE IF NOT EXISTS document_types (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
template_path TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`);
|
||||
)`);
|
||||
|
||||
// Таблица для документов
|
||||
db.run(`CREATE TABLE IF NOT EXISTS documents (
|
||||
// Таблица для документов
|
||||
db.run(`CREATE TABLE IF NOT EXISTS documents (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
@@ -229,10 +229,10 @@ db.run(`CREATE TABLE IF NOT EXISTS documents (
|
||||
FOREIGN KEY (approved_by) REFERENCES users (id),
|
||||
FOREIGN KEY (rejected_by) REFERENCES users (id),
|
||||
FOREIGN KEY (parent_document_id) REFERENCES documents (id)
|
||||
)`);
|
||||
)`);
|
||||
|
||||
// Таблица для этапов согласования
|
||||
db.run(`CREATE TABLE IF NOT EXISTS approval_stages (
|
||||
// Таблица для этапов согласования
|
||||
db.run(`CREATE TABLE IF NOT EXISTS approval_stages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
document_type_id INTEGER NOT NULL,
|
||||
stage_number INTEGER NOT NULL,
|
||||
@@ -248,10 +248,10 @@ db.run(`CREATE TABLE IF NOT EXISTS approval_stages (
|
||||
UNIQUE(document_type_id, stage_number),
|
||||
FOREIGN KEY (document_type_id) REFERENCES document_types (id),
|
||||
FOREIGN KEY (approver_user_id) REFERENCES users (id)
|
||||
)`);
|
||||
)`);
|
||||
|
||||
// Таблица для согласования документов
|
||||
db.run(`CREATE TABLE IF NOT EXISTS document_approvals (
|
||||
// Таблица для согласования документов
|
||||
db.run(`CREATE TABLE IF NOT EXISTS document_approvals (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
document_id INTEGER NOT NULL,
|
||||
stage_id INTEGER NOT NULL,
|
||||
@@ -268,10 +268,10 @@ db.run(`CREATE TABLE IF NOT EXISTS document_approvals (
|
||||
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (stage_id) REFERENCES approval_stages (id),
|
||||
FOREIGN KEY (approver_user_id) REFERENCES users (id)
|
||||
)`);
|
||||
)`);
|
||||
|
||||
// Таблица для комментариев к документам
|
||||
db.run(`CREATE TABLE IF NOT EXISTS document_comments (
|
||||
// Таблица для комментариев к документам
|
||||
db.run(`CREATE TABLE IF NOT EXISTS document_comments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
document_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
@@ -281,10 +281,10 @@ db.run(`CREATE TABLE IF NOT EXISTS document_comments (
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)`);
|
||||
)`);
|
||||
|
||||
// Таблица для истории изменений документов
|
||||
db.run(`CREATE TABLE IF NOT EXISTS document_history (
|
||||
// Таблица для истории изменений документов
|
||||
db.run(`CREATE TABLE IF NOT EXISTS document_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
document_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
@@ -294,9 +294,35 @@ db.run(`CREATE TABLE IF NOT EXISTS document_history (
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)`);
|
||||
)`);
|
||||
|
||||
console.log('✅ Таблицы для согласования документов созданы');
|
||||
console.log('✅ Таблицы для согласования документов созданы');
|
||||
|
||||
// ===== НОВЫЕ ТАБЛИЦЫ ДЛЯ ГРУПП ПОЛЬЗОВАТЕЛЕЙ =====
|
||||
|
||||
// Таблица для групп пользователей
|
||||
db.run(`CREATE TABLE IF NOT EXISTS user_groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
color TEXT DEFAULT '#3498db',
|
||||
can_approve_documents BOOLEAN DEFAULT false,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`);
|
||||
|
||||
// Таблица для связи пользователей с группами (многие-ко-многим)
|
||||
db.run(`CREATE TABLE IF NOT EXISTS user_group_memberships (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
group_id INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, group_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (group_id) REFERENCES user_groups (id) ON DELETE CASCADE
|
||||
)`);
|
||||
|
||||
console.log('✅ Таблицы для групп пользователей созданы');
|
||||
|
||||
// Запускаем проверку и обновление структуры таблиц
|
||||
setTimeout(() => {
|
||||
@@ -339,7 +365,11 @@ function checkAndUpdateTableStructure() {
|
||||
{ name: 'due_date', type: 'DATETIME' },
|
||||
{ name: 'rework_comment', type: 'TEXT' },
|
||||
{ name: 'closed_at', type: 'DATETIME' },
|
||||
{ name: 'closed_by', type: 'INTEGER' }
|
||||
{ name: 'closed_by', type: 'INTEGER' },
|
||||
// Новые колонки для типа задач
|
||||
{ name: 'task_type', type: 'TEXT DEFAULT "regular"' },
|
||||
{ name: 'approver_group_id', type: 'INTEGER' },
|
||||
{ name: 'document_id', type: 'INTEGER' }
|
||||
],
|
||||
task_assignments: [
|
||||
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||
@@ -386,6 +416,21 @@ function checkAndUpdateTableStructure() {
|
||||
{ name: 'vk_user_id', type: 'TEXT' },
|
||||
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
|
||||
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
|
||||
],
|
||||
user_groups: [
|
||||
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||
{ name: 'name', type: 'TEXT NOT NULL UNIQUE' },
|
||||
{ name: 'description', type: 'TEXT' },
|
||||
{ name: 'color', type: 'TEXT DEFAULT "#3498db"' },
|
||||
{ name: 'can_approve_documents', type: 'BOOLEAN DEFAULT false' },
|
||||
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
|
||||
{ name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
|
||||
],
|
||||
user_group_memberships: [
|
||||
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||
{ name: 'user_id', type: 'INTEGER NOT NULL' },
|
||||
{ name: 'group_id', type: 'INTEGER NOT NULL' },
|
||||
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
|
||||
]
|
||||
};
|
||||
|
||||
@@ -447,6 +492,46 @@ function checkAndUpdateTableStructure() {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Создаем индексы для новых таблиц
|
||||
setTimeout(() => {
|
||||
const newIndexes = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type)",
|
||||
"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)"
|
||||
];
|
||||
|
||||
newIndexes.forEach(indexQuery => {
|
||||
db.run(indexQuery, (err) => {
|
||||
if (err) {
|
||||
console.error(`❌ Ошибка создания индекса: ${err.message}`);
|
||||
} else {
|
||||
console.log(`✅ Индекс создан: ${indexQuery}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Создаем группу "Секретарь" по умолчанию, если её нет
|
||||
db.get("SELECT id FROM user_groups WHERE name = 'Секретарь'", (err, group) => {
|
||||
if (err || !group) {
|
||||
console.log('🔧 Создаем группу "Секретарь" по умолчанию...');
|
||||
db.run(
|
||||
`INSERT INTO user_groups (name, description, color, can_approve_documents)
|
||||
VALUES ('Секретарь', 'Группа для согласования документов', '#e74c3c', 1)`,
|
||||
(insertErr) => {
|
||||
if (insertErr) {
|
||||
console.error('❌ Ошибка создания группы "Секретарь":', insertErr.message);
|
||||
} else {
|
||||
console.log('✅ Группа "Секретарь" создана по умолчанию');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
}, 1000);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
@@ -621,7 +706,10 @@ async function createPostgresTables() {
|
||||
due_date TIMESTAMP,
|
||||
rework_comment TEXT,
|
||||
closed_at TIMESTAMP,
|
||||
closed_by INTEGER REFERENCES users(id)
|
||||
closed_by INTEGER REFERENCES users(id),
|
||||
task_type VARCHAR(50) DEFAULT 'regular',
|
||||
approver_group_id INTEGER,
|
||||
document_id INTEGER
|
||||
)
|
||||
`);
|
||||
|
||||
@@ -687,6 +775,129 @@ async function createPostgresTables() {
|
||||
)
|
||||
`);
|
||||
|
||||
// ===== НОВЫЕ ТАБЛИЦЫ ДЛЯ ГРУПП ПОЛЬЗОВАТЕЛЕЙ =====
|
||||
|
||||
// Таблица для групп пользователей
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_groups (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
color VARCHAR(20) DEFAULT '#3498db',
|
||||
can_approve_documents BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Таблица для связи пользователей с группами
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_group_memberships (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
group_id INTEGER NOT NULL REFERENCES user_groups(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, group_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Таблицы для документов (существующие)
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS document_types (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
template_path TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS documents (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
description TEXT,
|
||||
document_type_id INTEGER NOT NULL REFERENCES document_types(id),
|
||||
status VARCHAR(50) DEFAULT 'draft',
|
||||
created_by INTEGER NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
approved_at TIMESTAMP,
|
||||
approved_by INTEGER REFERENCES users(id),
|
||||
rejected_at TIMESTAMP,
|
||||
rejected_by INTEGER REFERENCES users(id),
|
||||
rejection_reason TEXT,
|
||||
file_path TEXT,
|
||||
file_name TEXT,
|
||||
file_size BIGINT,
|
||||
version INTEGER DEFAULT 1,
|
||||
parent_document_id INTEGER REFERENCES documents(id)
|
||||
)
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS approval_stages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
document_type_id INTEGER NOT NULL REFERENCES document_types(id),
|
||||
stage_number INTEGER NOT NULL,
|
||||
stage_name VARCHAR(100) NOT NULL,
|
||||
approver_role VARCHAR(50),
|
||||
approver_user_id INTEGER REFERENCES users(id),
|
||||
is_required BOOLEAN DEFAULT true,
|
||||
can_edit BOOLEAN DEFAULT false,
|
||||
can_comment BOOLEAN DEFAULT true,
|
||||
deadline_days INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(document_type_id, stage_number)
|
||||
)
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS document_approvals (
|
||||
id SERIAL PRIMARY KEY,
|
||||
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
stage_id INTEGER NOT NULL REFERENCES approval_stages(id),
|
||||
approver_user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
status VARCHAR(50) DEFAULT 'pending',
|
||||
comments TEXT,
|
||||
approved_at TIMESTAMP,
|
||||
rejected_at TIMESTAMP,
|
||||
deadline TIMESTAMP,
|
||||
notified_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(document_id, stage_id, approver_user_id)
|
||||
)
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS document_comments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
comment TEXT NOT NULL,
|
||||
is_internal BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS document_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
action VARCHAR(100) NOT NULL,
|
||||
changes TEXT,
|
||||
version INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
console.log('✅ Все таблицы PostgreSQL созданы/проверены');
|
||||
|
||||
// Создаем индексы
|
||||
const indexes = [
|
||||
'CREATE INDEX IF NOT EXISTS idx_tasks_created_by ON tasks(created_by)',
|
||||
@@ -698,7 +909,18 @@ async function createPostgresTables() {
|
||||
'CREATE INDEX IF NOT EXISTS idx_task_files_task_id ON task_files(task_id)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_activity_logs_task_id ON activity_logs(task_id)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)'
|
||||
'CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)',
|
||||
// Новые индексы
|
||||
'CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type)',
|
||||
'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_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)'
|
||||
];
|
||||
|
||||
for (const indexQuery of indexes) {
|
||||
@@ -715,6 +937,20 @@ async function createPostgresTables() {
|
||||
// Проверяем структуру PostgreSQL таблиц
|
||||
await checkPostgresTableStructure();
|
||||
|
||||
// Создаем группу "Секретарь" по умолчанию
|
||||
try {
|
||||
const checkResult = await client.query("SELECT id FROM user_groups WHERE name = 'Секретарь'");
|
||||
if (checkResult.rows.length === 0) {
|
||||
await client.query(`
|
||||
INSERT INTO user_groups (name, description, color, can_approve_documents)
|
||||
VALUES ('Секретарь', 'Группа для согласования документов', '#e74c3c', true)
|
||||
`);
|
||||
console.log('✅ Группа "Секретарь" создана по умолчанию');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Не удалось создать группу "Секретарь":', error.message);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка создания таблиц PostgreSQL:', error.message);
|
||||
}
|
||||
@@ -742,6 +978,11 @@ async function checkPostgresTableStructure() {
|
||||
{ name: 'vk_user_id', type: 'TEXT' },
|
||||
{ name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },
|
||||
{ name: 'updated_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }
|
||||
],
|
||||
tasks: [
|
||||
{ name: 'task_type', type: 'VARCHAR(50) DEFAULT "regular"' },
|
||||
{ name: 'approver_group_id', type: 'INTEGER' },
|
||||
{ name: 'document_id', type: 'INTEGER' }
|
||||
]
|
||||
};
|
||||
|
||||
@@ -799,6 +1040,96 @@ async function checkPostgresTableStructure() {
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательные функции для работы с группами пользователей
|
||||
function getUserGroups(userId, callback) {
|
||||
const query = `
|
||||
SELECT g.* FROM user_groups g
|
||||
JOIN user_group_memberships ugm ON g.id = ugm.group_id
|
||||
WHERE ugm.user_id = ?
|
||||
ORDER BY g.name
|
||||
`;
|
||||
|
||||
db.all(query, [userId], (err, groups) => {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка получения групп пользователя:', err);
|
||||
callback(err, []);
|
||||
} else {
|
||||
callback(null, groups || []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getGroupMembers(groupId, callback) {
|
||||
const query = `
|
||||
SELECT u.* FROM users u
|
||||
JOIN user_group_memberships ugm ON u.id = ugm.user_id
|
||||
WHERE ugm.group_id = ?
|
||||
ORDER BY u.name
|
||||
`;
|
||||
|
||||
db.all(query, [groupId], (err, members) => {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка получения участников группы:', err);
|
||||
callback(err, []);
|
||||
} else {
|
||||
callback(null, members || []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getApproverGroups(callback) {
|
||||
const query = `
|
||||
SELECT g.*, COUNT(DISTINCT ugm.user_id) as member_count
|
||||
FROM user_groups g
|
||||
LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id
|
||||
WHERE g.can_approve_documents = true
|
||||
GROUP BY g.id
|
||||
ORDER BY g.name
|
||||
`;
|
||||
|
||||
db.all(query, [], (err, groups) => {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка получения групп согласующих:', err);
|
||||
callback(err, []);
|
||||
} else {
|
||||
callback(null, groups || []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addUserToGroup(userId, groupId, callback) {
|
||||
const query = `
|
||||
INSERT INTO user_group_memberships (user_id, group_id)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (user_id, group_id) DO NOTHING
|
||||
`;
|
||||
|
||||
db.run(query, [userId, groupId], function(err) {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка добавления пользователя в группу:', err);
|
||||
callback(err);
|
||||
} else {
|
||||
callback(null, this.changes > 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeUserFromGroup(userId, groupId, callback) {
|
||||
const query = `
|
||||
DELETE FROM user_group_memberships
|
||||
WHERE user_id = ? AND group_id = ?
|
||||
`;
|
||||
|
||||
db.run(query, [userId, groupId], function(err) {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка удаления пользователя из группы:', err);
|
||||
callback(err);
|
||||
} else {
|
||||
callback(null, this.changes > 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createTaskFolder(taskId) {
|
||||
const taskFolder = path.join(tasksDir, taskId.toString());
|
||||
createDirIfNotExists(taskFolder);
|
||||
@@ -938,7 +1269,13 @@ module.exports = {
|
||||
checkTaskAccess,
|
||||
USE_POSTGRES,
|
||||
getDatabaseType: () => USE_POSTGRES ? 'PostgreSQL' : 'SQLite',
|
||||
checkAndUpdateTableStructure // Экспортируем для ручного запуска
|
||||
checkAndUpdateTableStructure, // Экспортируем для ручного запуска
|
||||
// Новые функции для работы с группами
|
||||
getUserGroups,
|
||||
getGroupMembers,
|
||||
getApproverGroups,
|
||||
addUserToGroup,
|
||||
removeUserFromGroup
|
||||
};
|
||||
|
||||
// Запускаем инициализацию при экспорте (но она завершится позже)
|
||||
|
||||
383
group-management.js
Normal file
383
group-management.js
Normal file
@@ -0,0 +1,383 @@
|
||||
// group-management.js
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
function setupGroupManagement(app, db) {
|
||||
|
||||
// Middleware для проверки администратора
|
||||
function requireAdmin(req, res, next) {
|
||||
if (!req.session.user || req.session.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Недостаточно прав' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// API для получения всех групп
|
||||
router.get('/api/groups', requireAdmin, (req, res) => {
|
||||
db.all(`
|
||||
SELECT g.*,
|
||||
COUNT(DISTINCT ugm.user_id) as member_count
|
||||
FROM user_groups g
|
||||
LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id
|
||||
GROUP BY g.id
|
||||
ORDER BY g.name
|
||||
`, [], (err, groups) => {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка получения групп:', err);
|
||||
return res.status(500).json({ error: 'Ошибка получения групп' });
|
||||
}
|
||||
res.json(groups || []);
|
||||
});
|
||||
});
|
||||
|
||||
// API для получения одной группы с участниками
|
||||
router.get('/api/groups/:groupId', requireAdmin, (req, res) => {
|
||||
const { groupId } = req.params;
|
||||
|
||||
db.get(`
|
||||
SELECT g.*,
|
||||
COUNT(DISTINCT ugm.user_id) as member_count
|
||||
FROM user_groups g
|
||||
LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id
|
||||
WHERE g.id = ?
|
||||
GROUP BY g.id
|
||||
`, [groupId], (err, group) => {
|
||||
if (err || !group) {
|
||||
return res.status(404).json({ error: 'Группа не найдена' });
|
||||
}
|
||||
|
||||
// Получаем участников группы
|
||||
db.all(`
|
||||
SELECT u.id, u.login, u.name, u.email, u.role
|
||||
FROM users u
|
||||
JOIN user_group_memberships ugm ON u.id = ugm.user_id
|
||||
WHERE ugm.group_id = ?
|
||||
ORDER BY u.name
|
||||
`, [groupId], (err, members) => {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка получения участников:', err);
|
||||
return res.status(500).json({ error: 'Ошибка получения участников' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
...group,
|
||||
members: members || []
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для создания новой группы
|
||||
router.post('/api/groups', requireAdmin, (req, res) => {
|
||||
const { name, description, color, can_approve_documents } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: 'Название группы обязательно' });
|
||||
}
|
||||
|
||||
db.run(`
|
||||
INSERT INTO user_groups (name, description, color, can_approve_documents)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, [
|
||||
name.trim(),
|
||||
description || '',
|
||||
color || '#3498db',
|
||||
can_approve_documents ? 1 : 0
|
||||
], function(err) {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка создания группы:', err);
|
||||
return res.status(500).json({
|
||||
error: 'Ошибка создания группы',
|
||||
details: err.message.includes('UNIQUE') ? 'Группа с таким названием уже существует' : err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
groupId: this.lastID,
|
||||
message: 'Группа создана успешно'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для обновления группы
|
||||
router.put('/api/groups/:groupId', requireAdmin, (req, res) => {
|
||||
const { groupId } = req.params;
|
||||
const { name, description, color, can_approve_documents } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: 'Название группы обязательно' });
|
||||
}
|
||||
|
||||
db.run(`
|
||||
UPDATE user_groups
|
||||
SET name = ?, description = ?, color = ?, can_approve_documents = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`, [
|
||||
name.trim(),
|
||||
description || '',
|
||||
color || '#3498db',
|
||||
can_approve_documents ? 1 : 0,
|
||||
groupId
|
||||
], function(err) {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка обновления группы:', err);
|
||||
return res.status(500).json({
|
||||
error: 'Ошибка обновления группы',
|
||||
details: err.message.includes('UNIQUE') ? 'Группа с таким названием уже существует' : err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({ error: 'Группа не найдена' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Группа обновлена успешно'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для удаления группы
|
||||
router.delete('/api/groups/:groupId', requireAdmin, (req, res) => {
|
||||
const { groupId } = req.params;
|
||||
|
||||
db.run(`DELETE FROM user_groups WHERE id = ?`, [groupId], function(err) {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка удаления группы:', err);
|
||||
return res.status(500).json({ error: 'Ошибка удаления группы' });
|
||||
}
|
||||
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({ error: 'Группа не найдена' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Группа удалена успешно'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для добавления пользователя в группу
|
||||
router.post('/api/groups/:groupId/members', requireAdmin, (req, res) => {
|
||||
const { groupId } = req.params;
|
||||
const { userId } = req.body;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({ error: 'ID пользователя обязательно' });
|
||||
}
|
||||
|
||||
// Проверяем существование группы и пользователя
|
||||
db.get(`SELECT id FROM user_groups WHERE id = ?`, [groupId], (err, group) => {
|
||||
if (err || !group) {
|
||||
return res.status(404).json({ error: 'Группа не найдена' });
|
||||
}
|
||||
|
||||
db.get(`SELECT id FROM users WHERE id = ?`, [userId], (err, user) => {
|
||||
if (err || !user) {
|
||||
return res.status(404).json({ error: 'Пользователь не найден' });
|
||||
}
|
||||
|
||||
db.run(`
|
||||
INSERT OR IGNORE INTO user_group_memberships (user_id, group_id)
|
||||
VALUES (?, ?)
|
||||
`, [userId, groupId], function(err) {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка добавления пользователя в группу:', err);
|
||||
return res.status(500).json({ error: 'Ошибка добавления пользователя в группу' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Пользователь добавлен в группу'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для удаления пользователя из группы
|
||||
router.delete('/api/groups/:groupId/members/:userId', requireAdmin, (req, res) => {
|
||||
const { groupId, userId } = req.params;
|
||||
|
||||
db.run(`
|
||||
DELETE FROM user_group_memberships
|
||||
WHERE user_id = ? AND group_id = ?
|
||||
`, [userId, groupId], function(err) {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка удаления пользователя из группы:', err);
|
||||
return res.status(500).json({ error: 'Ошибка удаления пользователя из группы' });
|
||||
}
|
||||
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({ error: 'Пользователь не найден в группе' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Пользователь удален из группы'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для получения всех пользователей с их группами
|
||||
router.get('/api/users-with-groups', requireAdmin, (req, res) => {
|
||||
db.all(`
|
||||
SELECT u.id, u.login, u.name, u.email, u.role,
|
||||
GROUP_CONCAT(DISTINCT g.name) as group_names,
|
||||
GROUP_CONCAT(DISTINCT g.id) as group_ids
|
||||
FROM users u
|
||||
LEFT JOIN user_group_memberships ugm ON u.id = ugm.user_id
|
||||
LEFT JOIN user_groups g ON ugm.group_id = g.id
|
||||
WHERE u.role IN ('admin', 'teacher')
|
||||
GROUP BY u.id
|
||||
ORDER BY u.name
|
||||
`, [], (err, users) => {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка получения пользователей с группами:', err);
|
||||
return res.status(500).json({ error: 'Ошибка получения данных' });
|
||||
}
|
||||
|
||||
// Преобразуем строки в массивы
|
||||
const formattedUsers = users.map(user => ({
|
||||
...user,
|
||||
group_names: user.group_names ? user.group_names.split(',') : [],
|
||||
group_ids: user.group_ids ? user.group_ids.split(',').map(id => parseInt(id)) : []
|
||||
}));
|
||||
|
||||
res.json(formattedUsers);
|
||||
});
|
||||
});
|
||||
|
||||
// API для получения доступных групп для пользователя
|
||||
router.get('/api/users/:userId/available-groups', requireAdmin, (req, res) => {
|
||||
const { userId } = req.params;
|
||||
|
||||
db.all(`
|
||||
SELECT g.*,
|
||||
CASE WHEN ugm.user_id IS NOT NULL THEN 1 ELSE 0 END as is_member
|
||||
FROM user_groups g
|
||||
LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id AND ugm.user_id = ?
|
||||
ORDER BY g.name
|
||||
`, [userId], (err, groups) => {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка получения групп:', err);
|
||||
return res.status(500).json({ error: 'Ошибка получения данных' });
|
||||
}
|
||||
|
||||
res.json(groups || []);
|
||||
});
|
||||
});
|
||||
|
||||
// API для массового обновления групп пользователя
|
||||
router.put('/api/users/:userId/groups', requireAdmin, (req, res) => {
|
||||
const { userId } = req.params;
|
||||
const { groupIds } = req.body; // массив ID групп
|
||||
|
||||
if (!Array.isArray(groupIds)) {
|
||||
return res.status(400).json({ error: 'groupIds должен быть массивом' });
|
||||
}
|
||||
|
||||
// Проверяем существование пользователя
|
||||
db.get(`SELECT id FROM users WHERE id = ?`, [userId], (err, user) => {
|
||||
if (err || !user) {
|
||||
return res.status(404).json({ error: 'Пользователь не найден' });
|
||||
}
|
||||
|
||||
db.serialize(() => {
|
||||
// Удаляем все текущие группы пользователя
|
||||
db.run(`DELETE FROM user_group_memberships WHERE user_id = ?`, [userId], (err) => {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка удаления старых групп:', err);
|
||||
return res.status(500).json({ error: 'Ошибка обновления групп' });
|
||||
}
|
||||
|
||||
// Добавляем новые группы
|
||||
if (groupIds.length > 0) {
|
||||
const placeholders = groupIds.map(() => '(?, ?)').join(',');
|
||||
const values = groupIds.reduce((acc, groupId) => {
|
||||
acc.push(userId, groupId);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
db.run(`
|
||||
INSERT INTO user_group_memberships (user_id, group_id)
|
||||
VALUES ${placeholders}
|
||||
`, values, (err) => {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка добавления новых групп:', err);
|
||||
return res.status(500).json({ error: 'Ошибка обновления групп' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Группы пользователя обновлены'
|
||||
});
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Группы пользователя обновлены'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для получения пользователей, которые могут согласовывать документы
|
||||
router.get('/api/approvers', requireAdmin, (req, res) => {
|
||||
db.all(`
|
||||
SELECT DISTINCT u.id, u.login, u.name, u.email, u.role,
|
||||
g.name as group_name, g.color as group_color
|
||||
FROM users u
|
||||
JOIN user_group_memberships ugm ON u.id = ugm.user_id
|
||||
JOIN user_groups g ON ugm.group_id = g.id
|
||||
WHERE g.can_approve_documents = 1
|
||||
ORDER BY u.name
|
||||
`, [], (err, approvers) => {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка получения согласующих:', err);
|
||||
return res.status(500).json({ error: 'Ошибка получения данных' });
|
||||
}
|
||||
|
||||
res.json(approvers || []);
|
||||
});
|
||||
});
|
||||
|
||||
// API для получения групп, которые могут согласовывать документы
|
||||
router.get('/api/approver-groups', (req, res) => {
|
||||
const isAdmin = req.session.user && req.session.user.role === 'admin';
|
||||
|
||||
db.all(`
|
||||
SELECT g.*,
|
||||
COUNT(DISTINCT ugm.user_id) as member_count
|
||||
FROM user_groups g
|
||||
LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id
|
||||
WHERE g.can_approve_documents = 1
|
||||
GROUP BY g.id
|
||||
ORDER BY g.name
|
||||
`, [], (err, groups) => {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка получения групп согласующих:', err);
|
||||
return res.status(500).json({ error: 'Ошибка получения данных' });
|
||||
}
|
||||
|
||||
// Для обычных пользователей скрываем ID пользователей в группах
|
||||
if (!isAdmin) {
|
||||
groups = groups.map(group => ({
|
||||
...group,
|
||||
members: undefined
|
||||
}));
|
||||
}
|
||||
|
||||
res.json(groups || []);
|
||||
});
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
module.exports = { setupGroupManagement };
|
||||
543
public/admin-groups.html
Normal file
543
public/admin-groups.html
Normal file
@@ -0,0 +1,543 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Управление группами пользователей</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<style>
|
||||
.group-color {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.badge-custom {
|
||||
font-size: 0.8em;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/admin">CRM Админ-панель</a>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin">Главная</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/profiles">Пользователи</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/admin/groups">Группы</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin-doc">Документы</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="navbar-text">
|
||||
<span id="currentUser"></span>
|
||||
<button class="btn btn-sm btn-outline-light ms-2" onclick="logout()">Выйти</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Управление группами пользователей</h5>
|
||||
<button class="btn btn-primary btn-sm" onclick="openCreateGroupModal()">
|
||||
<i class="bi bi-plus-circle"></i> Создать группу
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Описание</th>
|
||||
<th>Участников</th>
|
||||
<th>Может согласовывать</th>
|
||||
<th>Цвет</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="groupsTableBody">
|
||||
<!-- Группы будут загружены сюда -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Пользователи и их группы</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Пользователь</th>
|
||||
<th>Роль</th>
|
||||
<th>Группы</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersTableBody">
|
||||
<!-- Пользователи будут загружены сюда -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно создания/редактирования группы -->
|
||||
<div class="modal fade" id="groupModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="groupModalTitle">Создать группу</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="groupForm">
|
||||
<input type="hidden" id="groupId">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Название группы *</label>
|
||||
<input type="text" class="form-control" id="groupName" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Описание</label>
|
||||
<textarea class="form-control" id="groupDescription" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Цвет группы</label>
|
||||
<input type="color" class="form-control form-control-color" id="groupColor" value="#3498db">
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="canApproveDocuments">
|
||||
<label class="form-check-label">Может согласовывать документы</label>
|
||||
<small class="form-text text-muted d-block">
|
||||
Пользователи этой группы будут доступны для выбора при создании задач согласования документов
|
||||
</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveGroup()">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно управления группами пользователя -->
|
||||
<div class="modal fade" id="userGroupsModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Группы пользователя: <span id="userName"></span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="userGroupsList">
|
||||
<!-- Группы пользователя будут загружены сюда -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
let currentUser = null;
|
||||
let groups = [];
|
||||
let users = [];
|
||||
|
||||
// Загрузка данных при загрузке страницы
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await checkAuth();
|
||||
await loadGroups();
|
||||
await loadUsersWithGroups();
|
||||
});
|
||||
|
||||
// Проверка авторизации
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const response = await fetch('/api/user');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
currentUser = data.user;
|
||||
|
||||
if (currentUser.role !== 'admin') {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('currentUser').textContent =
|
||||
`${currentUser.name} (${currentUser.role})`;
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки авторизации:', error);
|
||||
window.location.href = '/';
|
||||
}
|
||||
}
|
||||
|
||||
// Выход из системы
|
||||
function logout() {
|
||||
fetch('/api/logout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
.then(() => {
|
||||
window.location.href = '/';
|
||||
});
|
||||
}
|
||||
|
||||
// Загрузка групп
|
||||
async function loadGroups() {
|
||||
try {
|
||||
const response = await fetch('/api/groups');
|
||||
if (response.ok) {
|
||||
groups = await response.json();
|
||||
renderGroupsTable();
|
||||
} else {
|
||||
showError('Ошибка загрузки групп');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки групп:', error);
|
||||
showError('Ошибка загрузки групп');
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка пользователей с группами
|
||||
async function loadUsersWithGroups() {
|
||||
try {
|
||||
const response = await fetch('/api/users-with-groups');
|
||||
if (response.ok) {
|
||||
users = await response.json();
|
||||
renderUsersTable();
|
||||
} else {
|
||||
showError('Ошибка загрузки пользователей');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки пользователей:', error);
|
||||
showError('Ошибка загрузки пользователей');
|
||||
}
|
||||
}
|
||||
|
||||
// Отображение таблицы групп
|
||||
function renderGroupsTable() {
|
||||
const tbody = document.getElementById('groupsTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (groups.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">
|
||||
Нет созданных групп
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
groups.forEach(group => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<span class="group-color" style="background-color: ${group.color}"></span>
|
||||
<strong>${group.name}</strong>
|
||||
</td>
|
||||
<td>${group.description || '-'}</td>
|
||||
<td>
|
||||
<span class="badge bg-primary">${group.member_count || 0}</span>
|
||||
</td>
|
||||
<td>
|
||||
${group.can_approve_documents
|
||||
? '<span class="badge bg-success">Да</span>'
|
||||
: '<span class="badge bg-secondary">Нет</span>'}
|
||||
</td>
|
||||
<td>
|
||||
<span class="group-color" style="background-color: ${group.color}"></span>
|
||||
${group.color}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary me-1" onclick="editGroup(${group.id})">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteGroup(${group.id})">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Отображение таблицы пользователей
|
||||
function renderUsersTable() {
|
||||
const tbody = document.getElementById('usersTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (users.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">
|
||||
Нет пользователей
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
users.forEach(user => {
|
||||
const groupsHtml = user.group_names.length > 0
|
||||
? user.group_names.map(name =>
|
||||
`<span class="badge bg-info me-1">${name}</span>`
|
||||
).join('')
|
||||
: '<span class="text-muted">Нет групп</span>';
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<div><strong>${user.name}</strong></div>
|
||||
<small class="text-muted">${user.login} • ${user.email}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge ${user.role === 'admin' ? 'bg-danger' : 'bg-secondary'}">
|
||||
${user.role === 'admin' ? 'Администратор' : 'Учитель'}
|
||||
</span>
|
||||
</td>
|
||||
<td>${groupsHtml}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="manageUserGroups(${user.id}, '${user.name}')">
|
||||
<i class="bi bi-person-gear"></i> Управление группами
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Открытие модального окна создания группы
|
||||
function openCreateGroupModal() {
|
||||
document.getElementById('groupModalTitle').textContent = 'Создать группу';
|
||||
document.getElementById('groupId').value = '';
|
||||
document.getElementById('groupName').value = '';
|
||||
document.getElementById('groupDescription').value = '';
|
||||
document.getElementById('groupColor').value = '#3498db';
|
||||
document.getElementById('canApproveDocuments').checked = false;
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('groupModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Редактирование группы
|
||||
async function editGroup(groupId) {
|
||||
const group = groups.find(g => g.id === groupId);
|
||||
if (!group) return;
|
||||
|
||||
document.getElementById('groupModalTitle').textContent = 'Редактировать группу';
|
||||
document.getElementById('groupId').value = group.id;
|
||||
document.getElementById('groupName').value = group.name;
|
||||
document.getElementById('groupDescription').value = group.description || '';
|
||||
document.getElementById('groupColor').value = group.color;
|
||||
document.getElementById('canApproveDocuments').checked = !!group.can_approve_documents;
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('groupModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Сохранение группы
|
||||
async function saveGroup() {
|
||||
const groupId = document.getElementById('groupId').value;
|
||||
const groupData = {
|
||||
name: document.getElementById('groupName').value.trim(),
|
||||
description: document.getElementById('groupDescription').value.trim(),
|
||||
color: document.getElementById('groupColor').value,
|
||||
can_approve_documents: document.getElementById('canApproveDocuments').checked
|
||||
};
|
||||
|
||||
if (!groupData.name) {
|
||||
showError('Введите название группы');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = groupId
|
||||
? `/api/groups/${groupId}`
|
||||
: '/api/groups';
|
||||
const method = groupId ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(groupData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
showSuccess(data.message || 'Группа сохранена');
|
||||
|
||||
// Закрываем модальное окно
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('groupModal'));
|
||||
modal.hide();
|
||||
|
||||
// Перезагружаем данные
|
||||
await loadGroups();
|
||||
await loadUsersWithGroups();
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
showError(errorData.error || 'Ошибка сохранения группы');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения группы:', error);
|
||||
showError('Ошибка сохранения группы');
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление группы
|
||||
async function deleteGroup(groupId) {
|
||||
if (!confirm('Вы уверены, что хотите удалить эту группу? Все связи с пользователями будут удалены.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/groups/${groupId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
showSuccess(data.message || 'Группа удалена');
|
||||
|
||||
// Перезагружаем данные
|
||||
await loadGroups();
|
||||
await loadUsersWithGroups();
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
showError(errorData.error || 'Ошибка удаления группы');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления группы:', error);
|
||||
showError('Ошибка удаления группы');
|
||||
}
|
||||
}
|
||||
|
||||
// Управление группами пользователя
|
||||
async function manageUserGroups(userId, userName) {
|
||||
document.getElementById('userName').textContent = userName;
|
||||
|
||||
try {
|
||||
// Получаем доступные группы для пользователя
|
||||
const response = await fetch(`/api/users/${userId}/available-groups`);
|
||||
if (response.ok) {
|
||||
const availableGroups = await response.json();
|
||||
|
||||
// Создаем список чекбоксов
|
||||
let groupsHtml = '';
|
||||
availableGroups.forEach(group => {
|
||||
groupsHtml += `
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
id="group_${group.id}"
|
||||
${group.is_member ? 'checked' : ''}
|
||||
onchange="toggleUserGroup(${userId}, ${group.id}, this.checked)">
|
||||
<label class="form-check-label" for="group_${group.id}">
|
||||
<span class="group-color" style="background-color: ${group.color}"></span>
|
||||
${group.name}
|
||||
${group.can_approve_documents
|
||||
? '<span class="badge bg-success badge-custom ms-1">Согласование</span>'
|
||||
: ''}
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
document.getElementById('userGroupsList').innerHTML = groupsHtml ||
|
||||
'<p class="text-muted">Нет доступных групп</p>';
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('userGroupsModal'));
|
||||
modal.show();
|
||||
} else {
|
||||
showError('Ошибка загрузки групп пользователя');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки групп:', error);
|
||||
showError('Ошибка загрузки групп');
|
||||
}
|
||||
}
|
||||
|
||||
// Переключение группы пользователя
|
||||
async function toggleUserGroup(userId, groupId, isChecked) {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}/groups`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
groupIds: isChecked
|
||||
? [...document.querySelectorAll('#userGroupsList input:checked')]
|
||||
.map(input => parseInt(input.id.split('_')[1]))
|
||||
: [...document.querySelectorAll('#userGroupsList input:checked')]
|
||||
.filter(input => parseInt(input.id.split('_')[1]) !== groupId)
|
||||
.map(input => parseInt(input.id.split('_')[1]))
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
showError(errorData.error || 'Ошибка обновления групп');
|
||||
// Возвращаем чекбокс в предыдущее состояние
|
||||
const checkbox = document.getElementById(`group_${groupId}`);
|
||||
checkbox.checked = !isChecked;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления групп:', error);
|
||||
showError('Ошибка обновления групп');
|
||||
// Возвращаем чекбокс в предыдущее состояние
|
||||
const checkbox = document.getElementById(`group_${groupId}`);
|
||||
checkbox.checked = !isChecked;
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательные функции для уведомлений
|
||||
function showSuccess(message) {
|
||||
alert(message); // Можно заменить на красивый toast
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
alert('Ошибка: ' + message); // Можно заменить на красивый toast
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -272,6 +272,7 @@
|
||||
<div class="user-info">
|
||||
<span id="current-user"></span>
|
||||
<button onclick="window.location.href = '/'">Назад к задачам</button>
|
||||
<li class="nav-item"><a class="nav-link" href="/admin/groups">Управление группами</a></li>
|
||||
<button onclick="logout()">Выйти</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -782,7 +782,13 @@ 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') {
|
||||
return res.status(403).send('Доступ запрещен');
|
||||
}
|
||||
res.sendFile(path.join(__dirname, 'public/admin-groups.html'));
|
||||
});
|
||||
// Инициализация сервера
|
||||
async function initializeServer() {
|
||||
console.log('🚀 Инициализация сервера...');
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
function getApproverUsers(groupId) {
|
||||
return new Promise((resolve) => {
|
||||
db.all(`
|
||||
SELECT u.id, u.login, u.name, u.email
|
||||
FROM users u
|
||||
JOIN user_group_memberships ugm ON u.id = ugm.user_id
|
||||
WHERE ugm.group_id = ?
|
||||
`, [groupId], (err, users) => {
|
||||
resolve(err ? [] : users);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupTaskEndpoints(app, db, upload) {
|
||||
const { logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database');
|
||||
const { sendTaskNotifications } = require('./notifications');
|
||||
@@ -444,7 +457,180 @@ function setupTaskEndpoints(app, db, upload) {
|
||||
});
|
||||
});
|
||||
});
|
||||
// API для создания задачи согласования документов
|
||||
app.post('/api/document-approval-tasks', requireAuth, upload.array('files', 15), (req, res) => {
|
||||
const { title, description, approverGroupId, documentId, dueDate } = req.body;
|
||||
const createdBy = req.session.user.id;
|
||||
|
||||
if (!title) {
|
||||
return res.status(400).json({ error: 'Название задачи обязательно' });
|
||||
}
|
||||
|
||||
if (!approverGroupId) {
|
||||
return res.status(400).json({ error: 'Выберите группу для согласования' });
|
||||
}
|
||||
|
||||
if (!dueDate) {
|
||||
return res.status(400).json({ error: 'Дата и время выполнения обязательны' });
|
||||
}
|
||||
|
||||
// Проверяем, может ли группа согласовывать документы
|
||||
db.get("SELECT can_approve_documents FROM user_groups WHERE id = ?",
|
||||
[approverGroupId], (err, group) => {
|
||||
if (err || !group || !group.can_approve_documents) {
|
||||
return res.status(400).json({ error: 'Выбранная группа не может согласовывать документы' });
|
||||
}
|
||||
|
||||
db.serialize(async () => {
|
||||
const startDate = new Date().toISOString();
|
||||
|
||||
// Создаем задачу с типом "document_approval"
|
||||
db.run(
|
||||
`INSERT INTO tasks (title, description, created_by, task_type,
|
||||
approver_group_id, document_id, start_date, due_date)
|
||||
VALUES (?, ?, ?, 'document_approval', ?, ?, ?, ?)`,
|
||||
[title, description, createdBy, approverGroupId, documentId || null, startDate, dueDate || null],
|
||||
async function(err) {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
return;
|
||||
}
|
||||
|
||||
const taskId = this.lastID;
|
||||
|
||||
// Сохраняем метаданные
|
||||
saveTaskMetadata(taskId, title, description, createdBy, null, startDate, dueDate);
|
||||
|
||||
// Получаем всех пользователей из группы согласующих
|
||||
const approvers = await getApproverUsers(approverGroupId);
|
||||
|
||||
// Назначаем задачу всем согласующим в группе
|
||||
if (approvers.length > 0) {
|
||||
approvers.forEach(approver => {
|
||||
db.run(
|
||||
"INSERT INTO task_assignments (task_id, user_id, status, start_date, due_date) VALUES (?, ?, 'assigned', ?, ?)",
|
||||
[taskId, approver.id, startDate, dueDate || null]
|
||||
);
|
||||
});
|
||||
|
||||
// Отправляем уведомления
|
||||
sendTaskNotifications('created', taskId, title, description, createdBy);
|
||||
}
|
||||
|
||||
logActivity(taskId, createdBy, 'DOCUMENT_APPROVAL_TASK_CREATED',
|
||||
`Создана задача согласования документа для группы ${approverGroupId}`);
|
||||
|
||||
// Загрузка файлов (если есть)
|
||||
if (req.files && req.files.length > 0) {
|
||||
const userFolder = createUserTaskFolder(taskId, req.session.user.login);
|
||||
|
||||
req.files.forEach(file => {
|
||||
const newPath = path.join(userFolder, path.basename(file.filename));
|
||||
fs.renameSync(file.path, newPath);
|
||||
|
||||
const originalName = file.originalname;
|
||||
|
||||
db.run(
|
||||
"INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[taskId, createdBy, path.basename(file.filename), originalName, newPath, file.size]
|
||||
);
|
||||
|
||||
logActivity(taskId, createdBy, 'FILE_UPLOADED', `Загружен файл: ${originalName}`);
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
taskId: taskId,
|
||||
message: 'Задача согласования документа создана',
|
||||
approversCount: approvers.length
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для получения задач согласования документов
|
||||
app.get('/api/document-approval-tasks', requireAuth, (req, res) => {
|
||||
const userId = req.session.user.id;
|
||||
|
||||
const query = `
|
||||
SELECT DISTINCT
|
||||
t.*,
|
||||
u.name as creator_name,
|
||||
u.login as creator_login,
|
||||
g.name as approver_group_name,
|
||||
g.color as approver_group_color,
|
||||
GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids,
|
||||
GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names
|
||||
FROM tasks t
|
||||
LEFT JOIN users u ON t.created_by = u.id
|
||||
LEFT JOIN user_groups g ON t.approver_group_id = g.id
|
||||
LEFT JOIN task_assignments ta ON t.id = ta.task_id
|
||||
LEFT JOIN users u2 ON ta.user_id = u2.id
|
||||
WHERE t.task_type = 'document_approval'
|
||||
AND t.status = 'active'
|
||||
AND t.closed_at IS NULL
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
|
||||
if (req.session.user.role !== 'admin') {
|
||||
query += ` AND (t.created_by = ? OR ta.user_id = ?)`;
|
||||
params.push(userId, userId);
|
||||
}
|
||||
|
||||
query += " GROUP BY t.id ORDER BY t.created_at DESC";
|
||||
|
||||
db.all(query, params, (err, tasks) => {
|
||||
if (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
return;
|
||||
}
|
||||
|
||||
const taskPromises = tasks.map(task => {
|
||||
return new Promise((resolve) => {
|
||||
// Получаем назначения
|
||||
db.all(`
|
||||
SELECT ta.*, u.name as user_name, u.login as user_login
|
||||
FROM task_assignments ta
|
||||
LEFT JOIN users u ON ta.user_id = u.id
|
||||
WHERE ta.task_id = ?
|
||||
`, [task.id], (err, assignments) => {
|
||||
if (err) {
|
||||
task.assignments = [];
|
||||
} else {
|
||||
assignments.forEach(assignment => {
|
||||
if (checkIfOverdue(assignment.due_date, assignment.status) && assignment.status !== 'completed') {
|
||||
assignment.status = 'overdue';
|
||||
}
|
||||
});
|
||||
task.assignments = assignments || [];
|
||||
}
|
||||
|
||||
// Получаем информацию о группе согласующих
|
||||
if (task.approver_group_id) {
|
||||
db.get(`
|
||||
SELECT name, color, can_approve_documents
|
||||
FROM user_groups
|
||||
WHERE id = ?
|
||||
`, [task.approver_group_id], (err, group) => {
|
||||
task.approver_group = group || null;
|
||||
resolve(task);
|
||||
});
|
||||
} else {
|
||||
resolve(task);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(taskPromises).then(completedTasks => {
|
||||
res.json(completedTasks);
|
||||
});
|
||||
});
|
||||
});
|
||||
// API для обновления статуса в Канбане
|
||||
app.put('/api/kanban-tasks/:taskId/status', requireAuth, (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
@@ -1163,4 +1349,4 @@ function setupTaskEndpoints(app, db, upload) {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { setupTaskEndpoints };
|
||||
module.exports = { setupTaskEndpoints,getApproverUsers };
|
||||
Reference in New Issue
Block a user