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
345
database.js
345
database.js
@@ -298,6 +298,32 @@ db.run(`CREATE TABLE IF NOT EXISTS document_history (
|
|||||||
|
|
||||||
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(() => {
|
setTimeout(() => {
|
||||||
checkAndUpdateTableStructure();
|
checkAndUpdateTableStructure();
|
||||||
@@ -339,7 +365,11 @@ function checkAndUpdateTableStructure() {
|
|||||||
{ name: 'due_date', type: 'DATETIME' },
|
{ name: 'due_date', type: 'DATETIME' },
|
||||||
{ name: 'rework_comment', type: 'TEXT' },
|
{ name: 'rework_comment', type: 'TEXT' },
|
||||||
{ name: 'closed_at', type: 'DATETIME' },
|
{ 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: [
|
task_assignments: [
|
||||||
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
{ name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
|
||||||
@@ -386,6 +416,21 @@ function checkAndUpdateTableStructure() {
|
|||||||
{ name: 'vk_user_id', type: 'TEXT' },
|
{ name: 'vk_user_id', type: 'TEXT' },
|
||||||
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
|
{ name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
|
||||||
{ name: 'updated_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);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,7 +706,10 @@ async function createPostgresTables() {
|
|||||||
due_date TIMESTAMP,
|
due_date TIMESTAMP,
|
||||||
rework_comment TEXT,
|
rework_comment TEXT,
|
||||||
closed_at TIMESTAMP,
|
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 = [
|
const indexes = [
|
||||||
'CREATE INDEX IF NOT EXISTS idx_tasks_created_by ON tasks(created_by)',
|
'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_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_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_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) {
|
for (const indexQuery of indexes) {
|
||||||
@@ -715,6 +937,20 @@ async function createPostgresTables() {
|
|||||||
// Проверяем структуру PostgreSQL таблиц
|
// Проверяем структуру PostgreSQL таблиц
|
||||||
await checkPostgresTableStructure();
|
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) {
|
} catch (error) {
|
||||||
console.error('❌ Ошибка создания таблиц PostgreSQL:', error.message);
|
console.error('❌ Ошибка создания таблиц PostgreSQL:', error.message);
|
||||||
}
|
}
|
||||||
@@ -742,6 +978,11 @@ async function checkPostgresTableStructure() {
|
|||||||
{ name: 'vk_user_id', type: 'TEXT' },
|
{ name: 'vk_user_id', type: 'TEXT' },
|
||||||
{ name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },
|
{ name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },
|
||||||
{ name: 'updated_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) {
|
function createTaskFolder(taskId) {
|
||||||
const taskFolder = path.join(tasksDir, taskId.toString());
|
const taskFolder = path.join(tasksDir, taskId.toString());
|
||||||
createDirIfNotExists(taskFolder);
|
createDirIfNotExists(taskFolder);
|
||||||
@@ -938,7 +1269,13 @@ module.exports = {
|
|||||||
checkTaskAccess,
|
checkTaskAccess,
|
||||||
USE_POSTGRES,
|
USE_POSTGRES,
|
||||||
getDatabaseType: () => USE_POSTGRES ? 'PostgreSQL' : 'SQLite',
|
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">
|
<div class="user-info">
|
||||||
<span id="current-user"></span>
|
<span id="current-user"></span>
|
||||||
<button onclick="window.location.href = '/'">Назад к задачам</button>
|
<button onclick="window.location.href = '/'">Назад к задачам</button>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/admin/groups">Управление группами</a></li>
|
||||||
<button onclick="logout()">Выйти</button>
|
<button onclick="logout()">Выйти</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -782,7 +782,13 @@ app.get('/api/email-health', requireAuth, async (req, res) => {
|
|||||||
res.status(500).json({ error: error.message });
|
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() {
|
async function initializeServer() {
|
||||||
console.log('🚀 Инициализация сервера...');
|
console.log('🚀 Инициализация сервера...');
|
||||||
|
|||||||
@@ -2,6 +2,19 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
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) {
|
function setupTaskEndpoints(app, db, upload) {
|
||||||
const { logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database');
|
const { logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database');
|
||||||
const { sendTaskNotifications } = require('./notifications');
|
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 для обновления статуса в Канбане
|
// API для обновления статуса в Канбане
|
||||||
app.put('/api/kanban-tasks/:taskId/status', requireAuth, (req, res) => {
|
app.put('/api/kanban-tasks/:taskId/status', requireAuth, (req, res) => {
|
||||||
const { taskId } = req.params;
|
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