This commit is contained in:
2026-01-27 00:32:06 +05:00
parent 30aa35357f
commit 0fe8f05b73
7 changed files with 2837 additions and 102 deletions

1279
admin-server.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -195,108 +195,134 @@ function createSQLiteTables() {
console.log('✅ Таблица для пользовательских настроек инициализирована');
// Таблица для типов документов
db.run(`CREATE TABLE IF NOT EXISTS document_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
template_path TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
db.run(`CREATE TABLE IF NOT EXISTS document_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
template_path TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// Таблица для документов
db.run(`CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
document_type_id INTEGER NOT NULL,
status TEXT DEFAULT 'draft',
created_by INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
approved_at DATETIME,
approved_by INTEGER,
rejected_at DATETIME,
rejected_by INTEGER,
rejection_reason TEXT,
file_path TEXT,
file_name TEXT,
file_size INTEGER,
version INTEGER DEFAULT 1,
parent_document_id INTEGER,
FOREIGN KEY (document_type_id) REFERENCES document_types (id),
FOREIGN KEY (created_by) REFERENCES users (id),
FOREIGN KEY (approved_by) REFERENCES users (id),
FOREIGN KEY (rejected_by) REFERENCES users (id),
FOREIGN KEY (parent_document_id) REFERENCES documents (id)
)`);
// Таблица для документов
db.run(`CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
document_type_id INTEGER NOT NULL,
status TEXT DEFAULT 'draft',
created_by INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
approved_at DATETIME,
approved_by INTEGER,
rejected_at DATETIME,
rejected_by INTEGER,
rejection_reason TEXT,
file_path TEXT,
file_name TEXT,
file_size INTEGER,
version INTEGER DEFAULT 1,
parent_document_id INTEGER,
FOREIGN KEY (document_type_id) REFERENCES document_types (id),
FOREIGN KEY (created_by) REFERENCES users (id),
FOREIGN KEY (approved_by) REFERENCES users (id),
FOREIGN KEY (rejected_by) REFERENCES users (id),
FOREIGN KEY (parent_document_id) REFERENCES documents (id)
)`);
// Таблица для этапов согласования
db.run(`CREATE TABLE IF NOT EXISTS approval_stages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_type_id INTEGER NOT NULL,
stage_number INTEGER NOT NULL,
stage_name TEXT NOT NULL,
approver_role TEXT,
approver_user_id INTEGER,
is_required BOOLEAN DEFAULT true,
can_edit BOOLEAN DEFAULT false,
can_comment BOOLEAN DEFAULT true,
deadline_days INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(document_type_id, stage_number),
FOREIGN KEY (document_type_id) REFERENCES document_types (id),
FOREIGN KEY (approver_user_id) REFERENCES users (id)
)`);
// Таблица для этапов согласования
db.run(`CREATE TABLE IF NOT EXISTS approval_stages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_type_id INTEGER NOT NULL,
stage_number INTEGER NOT NULL,
stage_name TEXT NOT NULL,
approver_role TEXT,
approver_user_id INTEGER,
is_required BOOLEAN DEFAULT true,
can_edit BOOLEAN DEFAULT false,
can_comment BOOLEAN DEFAULT true,
deadline_days INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(document_type_id, stage_number),
FOREIGN KEY (document_type_id) REFERENCES document_types (id),
FOREIGN KEY (approver_user_id) REFERENCES users (id)
)`);
// Таблица для согласования документов
db.run(`CREATE TABLE IF NOT EXISTS document_approvals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
stage_id INTEGER NOT NULL,
approver_user_id INTEGER NOT NULL,
status TEXT DEFAULT 'pending',
comments TEXT,
approved_at DATETIME,
rejected_at DATETIME,
deadline DATETIME,
notified_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(document_id, stage_id, approver_user_id),
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
FOREIGN KEY (stage_id) REFERENCES approval_stages (id),
FOREIGN KEY (approver_user_id) REFERENCES users (id)
)`);
// Таблица для согласования документов
db.run(`CREATE TABLE IF NOT EXISTS document_approvals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
stage_id INTEGER NOT NULL,
approver_user_id INTEGER NOT NULL,
status TEXT DEFAULT 'pending',
comments TEXT,
approved_at DATETIME,
rejected_at DATETIME,
deadline DATETIME,
notified_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(document_id, stage_id, approver_user_id),
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
FOREIGN KEY (stage_id) REFERENCES approval_stages (id),
FOREIGN KEY (approver_user_id) REFERENCES users (id)
)`);
// Таблица для комментариев к документам
db.run(`CREATE TABLE IF NOT EXISTS document_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
comment TEXT NOT NULL,
is_internal BOOLEAN DEFAULT false,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
// Таблица для комментариев к документам
db.run(`CREATE TABLE IF NOT EXISTS document_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
comment TEXT NOT NULL,
is_internal BOOLEAN DEFAULT false,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
// Таблица для истории изменений документов
db.run(`CREATE TABLE IF NOT EXISTS document_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
action TEXT NOT NULL,
changes TEXT,
version INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
// Таблица для истории изменений документов
db.run(`CREATE TABLE IF NOT EXISTS document_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
action TEXT NOT NULL,
changes TEXT,
version INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
console.log('✅ Таблицы для согласования документов созданы');
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
View 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
View 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>

View File

@@ -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>

View File

@@ -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('🚀 Инициализация сервера...');

View File

@@ -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 };