h
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ promt
|
||||
package-lock.json
|
||||
data
|
||||
promt
|
||||
LICENSE
|
||||
644
auth.js
644
auth.js
@@ -1,323 +1,323 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
class AuthService {
|
||||
constructor() {
|
||||
this.db = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
setDatabase(database) {
|
||||
this.db = database;
|
||||
this.initialized = true;
|
||||
console.log('✅ База данных установлена в AuthService');
|
||||
this.initUsers();
|
||||
}
|
||||
|
||||
async initUsers() {
|
||||
if (!this.db) {
|
||||
console.log('⚠️ База данных не установлена, откладываем создание пользователей');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Создаем пользователей из .env
|
||||
const users = [
|
||||
{
|
||||
login: process.env.USER_1_LOGIN,
|
||||
password: process.env.USER_1_PASSWORD,
|
||||
name: process.env.USER_1_NAME,
|
||||
email: process.env.USER_1_EMAIL,
|
||||
auth_type: 'local'
|
||||
},
|
||||
{
|
||||
login: process.env.USER_2_LOGIN,
|
||||
password: process.env.USER_2_PASSWORD,
|
||||
name: process.env.USER_2_NAME,
|
||||
email: process.env.USER_2_EMAIL,
|
||||
auth_type: 'local'
|
||||
},
|
||||
{
|
||||
login: process.env.USER_3_LOGIN,
|
||||
password: process.env.USER_3_PASSWORD,
|
||||
name: process.env.USER_3_NAME,
|
||||
email: process.env.USER_3_EMAIL,
|
||||
auth_type: 'local'
|
||||
}
|
||||
];
|
||||
|
||||
for (const userData of users) {
|
||||
if (userData.login && userData.password) {
|
||||
await this.createUserIfNotExists(userData);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка инициализации пользователей:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async createUserIfNotExists(userData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.db) {
|
||||
console.error('❌ База данных не доступна в createUserIfNotExists');
|
||||
reject(new Error('База данных не инициализирована'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.db.get("SELECT id FROM users WHERE login = ?", [userData.login], async (err, row) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
const hashedPassword = await bcrypt.hash(userData.password, 10);
|
||||
this.db.run(
|
||||
"INSERT INTO users (login, password, name, email, role, auth_type, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))",
|
||||
[
|
||||
userData.login,
|
||||
hashedPassword,
|
||||
userData.name,
|
||||
userData.email,
|
||||
'teacher',
|
||||
userData.auth_type || 'local'
|
||||
],
|
||||
function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(`✅ Создан пользователь: ${userData.name}`);
|
||||
resolve(this.lastID);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
resolve(row.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async authenticateLocal(login, password) {
|
||||
if (!this.db) {
|
||||
throw new Error('База данных не инициализирована в AuthService');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get("SELECT * FROM users WHERE login = ? AND auth_type = 'local'", [login], async (err, user) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
if (isValid) {
|
||||
// Обновляем last_login
|
||||
this.db.run("UPDATE users SET last_login = datetime('now') WHERE id = ?", [user.id]);
|
||||
|
||||
// Не возвращаем пароль
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
resolve(userWithoutPassword);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async authenticateLDAP(username, password) {
|
||||
if (!this.db) {
|
||||
throw new Error('База данных не инициализирована в AuthService');
|
||||
}
|
||||
|
||||
try {
|
||||
// Проверяем наличие URL для LDAP
|
||||
if (!process.env.LDAP_AUTH_URL) {
|
||||
console.log('⚠️ LDAP_AUTH_URL не задан в .env');
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await fetch(process.env.LDAP_AUTH_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.log(`⚠️ LDAP сервер вернул ошибку: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
return this.processLDAPUser(data);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка LDAP аутентификации:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async processLDAPUser(ldapData) {
|
||||
if (!this.db) {
|
||||
throw new Error('База данных не инициализирована в AuthService');
|
||||
}
|
||||
|
||||
const { username, full_name, groups, description } = ldapData;
|
||||
|
||||
// Определяем роль пользователя на основе групп
|
||||
const allowedGroups = process.env.ALLOWED_GROUPS ?
|
||||
process.env.ALLOWED_GROUPS.split(',').map(g => g.trim()) : [];
|
||||
|
||||
// ВАЖНО: Проверяем актуальные группы при каждом входе
|
||||
const isAdmin = groups && groups.some(group =>
|
||||
allowedGroups.includes(group)
|
||||
);
|
||||
|
||||
const role = isAdmin ? 'admin' : 'teacher';
|
||||
|
||||
// Сохраняем/обновляем пользователя в базе
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get("SELECT * FROM users WHERE login = ? AND auth_type = 'ldap'", [username], async (err, existingUser) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const userData = {
|
||||
login: username,
|
||||
name: full_name || username,
|
||||
email: `${username}@school25.ru`,
|
||||
role: role, // Всегда обновляем роль из актуальных групп
|
||||
auth_type: 'ldap',
|
||||
groups: groups ? JSON.stringify(groups) : '[]',
|
||||
description: description || '',
|
||||
last_login: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (existingUser) {
|
||||
// Всегда обновляем роль, даже если пользователь уже существует
|
||||
this.db.run(
|
||||
`UPDATE users SET
|
||||
name = ?, email = ?, role = ?, groups = ?, description = ?, last_login = datetime('now'),
|
||||
updated_at = datetime('now')
|
||||
WHERE id = ?`,
|
||||
[userData.name, userData.email, userData.role, userData.groups, userData.description, existingUser.id],
|
||||
function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(`✅ Обновлены данные LDAP пользователя ${username}. Роль: ${userData.role}, Группы: ${groups}`);
|
||||
resolve({
|
||||
id: existingUser.id,
|
||||
login: userData.login,
|
||||
name: userData.name,
|
||||
email: userData.email,
|
||||
role: userData.role,
|
||||
auth_type: userData.auth_type,
|
||||
groups: userData.groups,
|
||||
description: userData.description,
|
||||
last_login: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Создаем нового пользователя
|
||||
this.db.run(
|
||||
`INSERT INTO users (login, name, email, role, auth_type, groups, description, created_at, last_login)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||||
[userData.login, userData.name, userData.email, userData.role, userData.auth_type,
|
||||
userData.groups, userData.description],
|
||||
function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(`✅ Создан новый LDAP пользователь ${username}. Роль: ${userData.role}, Группы: ${groups}`);
|
||||
resolve({
|
||||
id: this.lastID,
|
||||
login: userData.login,
|
||||
name: userData.name,
|
||||
email: userData.email,
|
||||
role: userData.role,
|
||||
auth_type: userData.auth_type,
|
||||
groups: userData.groups,
|
||||
description: userData.description,
|
||||
last_login: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async authenticate(login, password) {
|
||||
if (!this.db) {
|
||||
throw new Error('База данных не инициализирована в AuthService');
|
||||
}
|
||||
|
||||
// Сначала пробуем локальную авторизацию
|
||||
try {
|
||||
const localUser = await this.authenticateLocal(login, password);
|
||||
if (localUser) {
|
||||
return localUser;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка локальной аутентификации:', error.message);
|
||||
}
|
||||
|
||||
// Если локальная не сработала, пробуем LDAP
|
||||
try {
|
||||
const ldapUser = await this.authenticateLDAP(login, password);
|
||||
if (ldapUser) {
|
||||
return ldapUser;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка LDAP аутентификации:', error.message);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getUserById(id) {
|
||||
if (!this.db) {
|
||||
throw new Error('База данных не инициализирована в AuthService');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get("SELECT id, login, name, email, role, auth_type, groups, description, last_login FROM users WHERE id = ?", [id], (err, user) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(user);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Метод для проверки готовности сервиса
|
||||
isReady() {
|
||||
return this.db !== null;
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем singleton экземпляр
|
||||
const authService = new AuthService();
|
||||
|
||||
const bcrypt = require('bcryptjs');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
class AuthService {
|
||||
constructor() {
|
||||
this.db = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
setDatabase(database) {
|
||||
this.db = database;
|
||||
this.initialized = true;
|
||||
console.log('✅ База данных установлена в AuthService');
|
||||
this.initUsers();
|
||||
}
|
||||
|
||||
async initUsers() {
|
||||
if (!this.db) {
|
||||
console.log('⚠️ База данных не установлена, откладываем создание пользователей');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Создаем пользователей из .env
|
||||
const users = [
|
||||
{
|
||||
login: process.env.USER_1_LOGIN,
|
||||
password: process.env.USER_1_PASSWORD,
|
||||
name: process.env.USER_1_NAME,
|
||||
email: process.env.USER_1_EMAIL,
|
||||
auth_type: 'local'
|
||||
},
|
||||
{
|
||||
login: process.env.USER_2_LOGIN,
|
||||
password: process.env.USER_2_PASSWORD,
|
||||
name: process.env.USER_2_NAME,
|
||||
email: process.env.USER_2_EMAIL,
|
||||
auth_type: 'local'
|
||||
},
|
||||
{
|
||||
login: process.env.USER_3_LOGIN,
|
||||
password: process.env.USER_3_PASSWORD,
|
||||
name: process.env.USER_3_NAME,
|
||||
email: process.env.USER_3_EMAIL,
|
||||
auth_type: 'local'
|
||||
}
|
||||
];
|
||||
|
||||
for (const userData of users) {
|
||||
if (userData.login && userData.password) {
|
||||
await this.createUserIfNotExists(userData);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка инициализации пользователей:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async createUserIfNotExists(userData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.db) {
|
||||
console.error('❌ База данных не доступна в createUserIfNotExists');
|
||||
reject(new Error('База данных не инициализирована'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.db.get("SELECT id FROM users WHERE login = ?", [userData.login], async (err, row) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
const hashedPassword = await bcrypt.hash(userData.password, 10);
|
||||
this.db.run(
|
||||
"INSERT INTO users (login, password, name, email, role, auth_type, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))",
|
||||
[
|
||||
userData.login,
|
||||
hashedPassword,
|
||||
userData.name,
|
||||
userData.email,
|
||||
'teacher',
|
||||
userData.auth_type || 'local'
|
||||
],
|
||||
function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(`✅ Создан пользователь: ${userData.name}`);
|
||||
resolve(this.lastID);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
resolve(row.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async authenticateLocal(login, password) {
|
||||
if (!this.db) {
|
||||
throw new Error('База данных не инициализирована в AuthService');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get("SELECT * FROM users WHERE login = ? AND auth_type = 'local'", [login], async (err, user) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
if (isValid) {
|
||||
// Обновляем last_login
|
||||
this.db.run("UPDATE users SET last_login = datetime('now') WHERE id = ?", [user.id]);
|
||||
|
||||
// Не возвращаем пароль
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
resolve(userWithoutPassword);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async authenticateLDAP(username, password) {
|
||||
if (!this.db) {
|
||||
throw new Error('База данных не инициализирована в AuthService');
|
||||
}
|
||||
|
||||
try {
|
||||
// Проверяем наличие URL для LDAP
|
||||
if (!process.env.LDAP_AUTH_URL) {
|
||||
console.log('⚠️ LDAP_AUTH_URL не задан в .env');
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await fetch(process.env.LDAP_AUTH_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.log(`⚠️ LDAP сервер вернул ошибку: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
return this.processLDAPUser(data);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка LDAP аутентификации:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async processLDAPUser(ldapData) {
|
||||
if (!this.db) {
|
||||
throw new Error('База данных не инициализирована в AuthService');
|
||||
}
|
||||
|
||||
const { username, full_name, groups, description } = ldapData;
|
||||
|
||||
// Определяем роль пользователя на основе групп
|
||||
const allowedGroups = process.env.ALLOWED_GROUPS ?
|
||||
process.env.ALLOWED_GROUPS.split(',').map(g => g.trim()) : [];
|
||||
|
||||
// ВАЖНО: Проверяем актуальные группы при каждом входе
|
||||
const isAdmin = groups && groups.some(group =>
|
||||
allowedGroups.includes(group)
|
||||
);
|
||||
|
||||
const role = isAdmin ? 'admin' : 'teacher';
|
||||
|
||||
// Сохраняем/обновляем пользователя в базе
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get("SELECT * FROM users WHERE login = ? AND auth_type = 'ldap'", [username], async (err, existingUser) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const userData = {
|
||||
login: username,
|
||||
name: full_name || username,
|
||||
email: `${username}@school25.ru`,
|
||||
role: role, // Всегда обновляем роль из актуальных групп
|
||||
auth_type: 'ldap',
|
||||
groups: groups ? JSON.stringify(groups) : '[]',
|
||||
description: description || '',
|
||||
last_login: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (existingUser) {
|
||||
// Всегда обновляем роль, даже если пользователь уже существует
|
||||
this.db.run(
|
||||
`UPDATE users SET
|
||||
name = ?, email = ?, role = ?, groups = ?, description = ?, last_login = datetime('now'),
|
||||
updated_at = datetime('now')
|
||||
WHERE id = ?`,
|
||||
[userData.name, userData.email, userData.role, userData.groups, userData.description, existingUser.id],
|
||||
function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(`✅ Обновлены данные LDAP пользователя ${username}. Роль: ${userData.role}, Группы: ${groups}`);
|
||||
resolve({
|
||||
id: existingUser.id,
|
||||
login: userData.login,
|
||||
name: userData.name,
|
||||
email: userData.email,
|
||||
role: userData.role,
|
||||
auth_type: userData.auth_type,
|
||||
groups: userData.groups,
|
||||
description: userData.description,
|
||||
last_login: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Создаем нового пользователя
|
||||
this.db.run(
|
||||
`INSERT INTO users (login, name, email, role, auth_type, groups, description, created_at, last_login)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`,
|
||||
[userData.login, userData.name, userData.email, userData.role, userData.auth_type,
|
||||
userData.groups, userData.description],
|
||||
function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(`✅ Создан новый LDAP пользователь ${username}. Роль: ${userData.role}, Группы: ${groups}`);
|
||||
resolve({
|
||||
id: this.lastID,
|
||||
login: userData.login,
|
||||
name: userData.name,
|
||||
email: userData.email,
|
||||
role: userData.role,
|
||||
auth_type: userData.auth_type,
|
||||
groups: userData.groups,
|
||||
description: userData.description,
|
||||
last_login: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async authenticate(login, password) {
|
||||
if (!this.db) {
|
||||
throw new Error('База данных не инициализирована в AuthService');
|
||||
}
|
||||
|
||||
// Сначала пробуем локальную авторизацию
|
||||
try {
|
||||
const localUser = await this.authenticateLocal(login, password);
|
||||
if (localUser) {
|
||||
return localUser;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка локальной аутентификации:', error.message);
|
||||
}
|
||||
|
||||
// Если локальная не сработала, пробуем LDAP
|
||||
try {
|
||||
const ldapUser = await this.authenticateLDAP(login, password);
|
||||
if (ldapUser) {
|
||||
return ldapUser;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка LDAP аутентификации:', error.message);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getUserById(id) {
|
||||
if (!this.db) {
|
||||
throw new Error('База данных не инициализирована в AuthService');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get("SELECT id, login, name, email, role, auth_type, groups, description, last_login FROM users WHERE id = ?", [id], (err, user) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(user);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Метод для проверки готовности сервиса
|
||||
isReady() {
|
||||
return this.db !== null;
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем singleton экземпляр
|
||||
const authService = new AuthService();
|
||||
|
||||
module.exports = authService;
|
||||
1684
database.js
1684
database.js
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"name": "school-crm",
|
||||
"version": "1.2.0",
|
||||
"description": "CRM система для школы с управлением задачами и админ-панелью",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "~2.4.3",
|
||||
"dotenv": "~16.3.1",
|
||||
"express": "^4.21.2",
|
||||
"express-session": "^1.18.2",
|
||||
"form-data": "^4.0.0",
|
||||
"multer": "^2.0.2",
|
||||
"node-fetch": "~2.6.7",
|
||||
"nodemailer": "^6.9.13",
|
||||
"pg": "^8.11.3",
|
||||
"sqlite3": "~5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "~3.0.1"
|
||||
}
|
||||
{
|
||||
"name": "school-crm",
|
||||
"version": "1.2.0",
|
||||
"description": "CRM система для школы с управлением задачами и админ-панелью",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "~2.4.3",
|
||||
"dotenv": "~16.3.1",
|
||||
"express": "^4.21.2",
|
||||
"express-session": "^1.18.2",
|
||||
"form-data": "^4.0.0",
|
||||
"multer": "^2.0.2",
|
||||
"node-fetch": "~2.6.7",
|
||||
"nodemailer": "^6.9.13",
|
||||
"pg": "^8.11.3",
|
||||
"sqlite3": "~5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "~3.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,369 +1,369 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>School CRM - Управление задачами</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="login-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2><i class="fas fa-sign-in-alt"></i> Вход в School CRM</h2>
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<label for="login"><i class="fas fa-user"></i> Логин:</label>
|
||||
<input type="text" id="login" name="login" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password"><i class="fas fa-lock"></i> Пароль:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-sign-in-alt"></i> Войти
|
||||
</button>
|
||||
</form>
|
||||
<div class="test-users">
|
||||
<h3><i class="fas fa-users"></i> Управление задачами</h3>
|
||||
<ul>
|
||||
<li><strong><i class="fas fa-school"></i> @2025</strong> МАОУ - СОШ № 25</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="header-top">
|
||||
<h1><i class="fas fa-tasks"></i> School CRM - Управление задачами</h1>
|
||||
<div class="user-info">
|
||||
<span id="current-user"></span>
|
||||
<button onclick="logout()" class="btn-logout">
|
||||
<i class="fas fa-sign-out-alt"></i> Выйти
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<button onclick="showSection('tasks')" class="nav-btn">
|
||||
<i class="fas fa-list"></i> Задачи
|
||||
</button>
|
||||
<button onclick="showSection('create-task')" class="nav-btn">
|
||||
<i class="fas fa-plus-circle"></i> Создать задачу
|
||||
</button>
|
||||
<button onclick="showTasksWithoutDate()" class="nav-btn" id="tasks-no-date-btn">
|
||||
<i class="fas fa-clock"></i> Задачи без срока
|
||||
</button>
|
||||
<button onclick="showKanbanSection()" class="nav-btn">
|
||||
<i class="fas fa-columns"></i> Канбан
|
||||
</button>
|
||||
<button onclick="showSection('profile')" class="nav-btn" id="profile-btn">
|
||||
<i class="fas fa-user-circle"></i> Личный кабинет
|
||||
</button>
|
||||
<!--
|
||||
<button onclick="showSection('logs')" class="nav-btn">
|
||||
<i class="fas fa-history"></i> Лог активности
|
||||
</button>
|
||||
<button onclick="window.location.href = '/admin'" class="nav-btn btn-admin">
|
||||
<i class="fas fa-cog"></i> Админ-панель
|
||||
</button>
|
||||
-->
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section id="tasks-section" class="section">
|
||||
<h2><i class="fas fa-tasks"></i> Все задачи</h2>
|
||||
<div id="tasks-controls">
|
||||
<div class="filters">
|
||||
<div class="filter-group">
|
||||
<label for="search-tasks"><i class="fas fa-search"></i> Поиск:</label>
|
||||
<input type="text" id="search-tasks" placeholder="Поиск по названию и описанию..." oninput="loadTasks()">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="status-filter"><i class="fas fa-filter"></i> Статус:</label>
|
||||
<select id="status-filter" onchange="loadTasks()">
|
||||
<option value="active,in_progress,assigned,overdue,rework">Все активные</option>
|
||||
<option value="all">Все статусы</option>
|
||||
<option value="assigned">Назначена</option>
|
||||
<option value="in_progress">В работе</option>
|
||||
<option value="rework">На доработке</option>
|
||||
<option value="overdue">Просрочена</option>
|
||||
<option value="completed">Выполнена</option>
|
||||
<option value="closed">Закрыта</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="creator-filter"><i class="fas fa-user-tie"></i> Заказчик:</label>
|
||||
<select id="creator-filter" onchange="loadTasks()">
|
||||
<option value="">Все заказчики</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="assignee-filter"><i class="fas fa-user-check"></i> Исполнитель:</label>
|
||||
<select id="assignee-filter" onchange="loadTasks()">
|
||||
<option value="">Все исполнители</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="deadline-filter"><i class="fas fa-calendar-times"></i> Срок выполнения:</label>
|
||||
<select id="deadline-filter" onchange="loadTasks()">
|
||||
<option value="">Все сроки</option>
|
||||
<option value="48h">Менее 48 часов</option>
|
||||
<option value="24h">Менее 24 часов</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<label class="show-deleted-label" style="display: none;">
|
||||
<input type="checkbox" id="show-deleted" onchange="loadTasks()">
|
||||
<i class="fas fa-trash"></i> Показать удаленные задачи
|
||||
</label>
|
||||
</div>
|
||||
<div id="tasks-list"></div>
|
||||
</section>
|
||||
|
||||
<section id="create-task-section" class="section">
|
||||
<h2><i class="fas fa-plus-circle"></i> Создать новую задачу</h2>
|
||||
<form id="create-task-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="title"><i class="fas fa-heading"></i> Название задачи:</label>
|
||||
<input type="text" id="title" name="title" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description"><i class="fas fa-align-left"></i> Описание:</label>
|
||||
<textarea id="description" name="description" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="due-date"><i class="fas fa-calendar-alt"></i> Дата и время выполнения:</label>
|
||||
<input type="datetime-local" id="due-date" name="dueDate" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label><i class="fas fa-users"></i> Исполнители:</label>
|
||||
<div class="user-search">
|
||||
<input type="text" id="user-search" placeholder="Поиск исполнителей..." oninput="filterUsers()">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
<div id="users-checklist" class="checkbox-group"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="files"><i class="fas fa-paperclip"></i> Прикрепить файлы (до 15 файлов, максимум 300MB):</label>
|
||||
<div class="file-upload">
|
||||
<input type="file" id="files" name="files" multiple>
|
||||
<label for="files" class="file-upload-label">
|
||||
<i class="fas fa-cloud-upload-alt"></i> Выберите файлы
|
||||
</label>
|
||||
</div>
|
||||
<div id="file-list"></div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-check-circle"></i> Создать задачу
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="logs-section" class="section">
|
||||
<h2><i class="fas fa-history"></i> Лог активности</h2>
|
||||
<div id="logs-list"></div>
|
||||
</section>
|
||||
|
||||
<section id="profile-section" class="section">
|
||||
<h2><i class="fas fa-user-circle"></i> Личный кабинет</h2>
|
||||
|
||||
<div class="notification-settings">
|
||||
<h3><i class="fas fa-bell"></i> Настройки уведомлений</h3>
|
||||
<form id="notification-settings-form">
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="email-notifications" name="email_notifications">
|
||||
<span><i class="fas fa-envelope"></i> Email уведомления</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notification-email"><i class="fas fa-at"></i> Email для уведомлений:</label>
|
||||
<div class="input-with-icon">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<input type="email" id="notification-email" name="notification_email"
|
||||
placeholder="Введите email для уведомлений">
|
||||
</div>
|
||||
<small>Если не указано, будет использован email из профиля</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="telegram-notifications" name="telegram_notifications" disabled>
|
||||
<span><i class="fab fa-telegram"></i> Telegram уведомления (скоро)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="vk-notifications" name="vk_notifications" disabled>
|
||||
<span><i class="fab fa-vk"></i> ВКонтакте уведомления (скоро)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group"><label class="checkbox-label">
|
||||
<input type="checkbox" id="sberbank-notifications" name="sberbank_notifications" disabled>
|
||||
<span><i class="fas fa-university"></i> Сбербанк Онлайн уведомления (скоро)</span>
|
||||
</label></div>
|
||||
<div class="form-group"><label class="checkbox-label">
|
||||
<input type="checkbox" id="yandex-notifications" name="yandex_notifications" disabled>
|
||||
<span><i class="fab fa-yandex"></i> Яндекс уведомления (скоро)</span>
|
||||
</label></div>
|
||||
<div class="form-group"><label class="checkbox-label">
|
||||
<input type="checkbox" id="gosuslugi-notifications" name="gosuslugi_notifications" disabled>
|
||||
<span><i class="fas fa-passport"></i> Госуслуги уведомления (скоро)</span>
|
||||
</label></div>
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-save"></i> Сохранить настройки
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Модальные окна остаются без изменений -->
|
||||
<div id="edit-task-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeEditModal()">×</span>
|
||||
<h3><i class="fas fa-edit"></i> Редактировать задачу</h3>
|
||||
<form id="edit-task-form" enctype="multipart/form-data">
|
||||
<input type="hidden" id="edit-task-id">
|
||||
<div class="form-group">
|
||||
<label for="edit-title">Название задачи:</label>
|
||||
<input type="text" id="edit-title" name="title" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-description">Описание:</label>
|
||||
<textarea id="edit-description" name="description" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-due-date">Дата и время выполнения:</label>
|
||||
<input type="datetime-local" id="edit-due-date" name="dueDate" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Исполнители:</label>
|
||||
<div class="user-search">
|
||||
<input type="text" id="edit-user-search" placeholder="Поиск исполнителей..." oninput="filterEditUsers()">
|
||||
</div>
|
||||
<div id="edit-users-checklist" class="checkbox-group"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-files">Добавить файлы:</label>
|
||||
<input type="file" id="edit-files" name="files" multiple>
|
||||
<div id="edit-file-list"></div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-save"></i> Сохранить изменения
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="copy-task-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeCopyModal()">×</span>
|
||||
<h3><i class="fas fa-copy"></i> Создать копию задачи</h3>
|
||||
<form id="copy-task-form">
|
||||
<input type="hidden" id="copy-task-id">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="copy-due-date">Дата и время выполнения для копии:</label>
|
||||
<input type="datetime-local" id="copy-due-date" name="dueDate" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Назначить исполнителей для копии:</label>
|
||||
<div class="user-search">
|
||||
<input type="text" id="copy-user-search" placeholder="Поиск исполнителей..." oninput="filterCopyUsers()">
|
||||
</div>
|
||||
<div id="copy-users-checklist" class="checkbox-group"></div>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-copy"></i> Создать копию
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="edit-assignment-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeEditAssignmentModal()">×</span>
|
||||
<h3><i class="fas fa-clock"></i> Редактировать сроки исполнителя</h3>
|
||||
<form id="edit-assignment-form">
|
||||
<input type="hidden" id="edit-assignment-task-id">
|
||||
<input type="hidden" id="edit-assignment-user-id">
|
||||
<div class="form-group">
|
||||
<label for="edit-assignment-due-date">Дата и время выполнения:</label>
|
||||
<input type="datetime-local" id="edit-assignment-due-date" name="dueDate" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-save"></i> Сохранить сроки
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rework-task-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeReworkModal()">×</span>
|
||||
<h3><i class="fas fa-redo"></i> Вернуть задачу на доработку</h3>
|
||||
<form id="rework-task-form">
|
||||
<input type="hidden" id="rework-task-id">
|
||||
<div class="form-group">
|
||||
<label for="rework-comment">Комментарий к доработке:</label>
|
||||
<textarea id="rework-comment" name="comment" rows="4" placeholder="Укажите, что нужно исправить..." required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn-warning">
|
||||
<i class="fas fa-redo"></i> Вернуть на доработку
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kanban-section" class="section kanban-section">
|
||||
<div class="section-header">
|
||||
<h2><i class="fas fa-columns"></i> Канбан-доска</h2>
|
||||
<p>Перетаскивайте задачи между колонками для изменения статуса</p>
|
||||
<div class="kanban-controls">
|
||||
<div class="kanban-filters">
|
||||
<select id="kanban-filter" onchange="loadKanbanBoard()">
|
||||
<option value="all">Все задачи</option>
|
||||
<option value="created">Мои задачи (я создал)</option>
|
||||
<option value="assigned">Назначенные мне</option>
|
||||
</select>
|
||||
<select id="kanban-days" onchange="loadKanbanBoard()">
|
||||
<option value="7">7 дней</option>
|
||||
<option value="14">14 дней</option>
|
||||
<option value="30">30 дней</option>
|
||||
<option value="365">Все задачи</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kanban-board" class="kanban-board">
|
||||
<div class="loading">Загрузка Канбан-доски...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="auth.js"></script>
|
||||
<script src="users.js"></script>
|
||||
<script src="tasks.js"></script>
|
||||
<script src="kanban.js"></script>
|
||||
<script src="files.js"></script>
|
||||
<script src="profile.js"></script>
|
||||
<script src="ui.js"></script>
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>School CRM - Управление задачами</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="login-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2><i class="fas fa-sign-in-alt"></i> Вход в School CRM</h2>
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<label for="login"><i class="fas fa-user"></i> Логин:</label>
|
||||
<input type="text" id="login" name="login" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password"><i class="fas fa-lock"></i> Пароль:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-sign-in-alt"></i> Войти
|
||||
</button>
|
||||
</form>
|
||||
<div class="test-users">
|
||||
<h3><i class="fas fa-users"></i> Управление задачами</h3>
|
||||
<ul>
|
||||
<li><strong><i class="fas fa-school"></i> @2025</strong> МАОУ - СОШ № 25</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="header-top">
|
||||
<h1><i class="fas fa-tasks"></i> School CRM - Управление задачами</h1>
|
||||
<div class="user-info">
|
||||
<span id="current-user"></span>
|
||||
<button onclick="logout()" class="btn-logout">
|
||||
<i class="fas fa-sign-out-alt"></i> Выйти
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<button onclick="showSection('tasks')" class="nav-btn">
|
||||
<i class="fas fa-list"></i> Задачи
|
||||
</button>
|
||||
<button onclick="showSection('create-task')" class="nav-btn">
|
||||
<i class="fas fa-plus-circle"></i> Создать задачу
|
||||
</button>
|
||||
<button onclick="showTasksWithoutDate()" class="nav-btn" id="tasks-no-date-btn">
|
||||
<i class="fas fa-clock"></i> Задачи без срока
|
||||
</button>
|
||||
<button onclick="showKanbanSection()" class="nav-btn">
|
||||
<i class="fas fa-columns"></i> Канбан
|
||||
</button>
|
||||
<button onclick="showSection('profile')" class="nav-btn" id="profile-btn">
|
||||
<i class="fas fa-user-circle"></i> Личный кабинет
|
||||
</button>
|
||||
<!--
|
||||
<button onclick="showSection('logs')" class="nav-btn">
|
||||
<i class="fas fa-history"></i> Лог активности
|
||||
</button>
|
||||
<button onclick="window.location.href = '/admin'" class="nav-btn btn-admin">
|
||||
<i class="fas fa-cog"></i> Админ-панель
|
||||
</button>
|
||||
-->
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section id="tasks-section" class="section">
|
||||
<h2><i class="fas fa-tasks"></i> Все задачи</h2>
|
||||
<div id="tasks-controls">
|
||||
<div class="filters">
|
||||
<div class="filter-group">
|
||||
<label for="search-tasks"><i class="fas fa-search"></i> Поиск:</label>
|
||||
<input type="text" id="search-tasks" placeholder="Поиск по названию и описанию..." oninput="loadTasks()">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="status-filter"><i class="fas fa-filter"></i> Статус:</label>
|
||||
<select id="status-filter" onchange="loadTasks()">
|
||||
<option value="active,in_progress,assigned,overdue,rework">Все активные</option>
|
||||
<option value="all">Все статусы</option>
|
||||
<option value="assigned">Назначена</option>
|
||||
<option value="in_progress">В работе</option>
|
||||
<option value="rework">На доработке</option>
|
||||
<option value="overdue">Просрочена</option>
|
||||
<option value="completed">Выполнена</option>
|
||||
<option value="closed">Закрыта</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="creator-filter"><i class="fas fa-user-tie"></i> Заказчик:</label>
|
||||
<select id="creator-filter" onchange="loadTasks()">
|
||||
<option value="">Все заказчики</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="assignee-filter"><i class="fas fa-user-check"></i> Исполнитель:</label>
|
||||
<select id="assignee-filter" onchange="loadTasks()">
|
||||
<option value="">Все исполнители</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="deadline-filter"><i class="fas fa-calendar-times"></i> Срок выполнения:</label>
|
||||
<select id="deadline-filter" onchange="loadTasks()">
|
||||
<option value="">Все сроки</option>
|
||||
<option value="48h">Менее 48 часов</option>
|
||||
<option value="24h">Менее 24 часов</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<label class="show-deleted-label" style="display: none;">
|
||||
<input type="checkbox" id="show-deleted" onchange="loadTasks()">
|
||||
<i class="fas fa-trash"></i> Показать удаленные задачи
|
||||
</label>
|
||||
</div>
|
||||
<div id="tasks-list"></div>
|
||||
</section>
|
||||
|
||||
<section id="create-task-section" class="section">
|
||||
<h2><i class="fas fa-plus-circle"></i> Создать новую задачу</h2>
|
||||
<form id="create-task-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="title"><i class="fas fa-heading"></i> Название задачи:</label>
|
||||
<input type="text" id="title" name="title" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description"><i class="fas fa-align-left"></i> Описание:</label>
|
||||
<textarea id="description" name="description" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="due-date"><i class="fas fa-calendar-alt"></i> Дата и время выполнения:</label>
|
||||
<input type="datetime-local" id="due-date" name="dueDate" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label><i class="fas fa-users"></i> Исполнители:</label>
|
||||
<div class="user-search">
|
||||
<input type="text" id="user-search" placeholder="Поиск исполнителей..." oninput="filterUsers()">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
<div id="users-checklist" class="checkbox-group"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="files"><i class="fas fa-paperclip"></i> Прикрепить файлы (до 15 файлов, максимум 300MB):</label>
|
||||
<div class="file-upload">
|
||||
<input type="file" id="files" name="files" multiple>
|
||||
<label for="files" class="file-upload-label">
|
||||
<i class="fas fa-cloud-upload-alt"></i> Выберите файлы
|
||||
</label>
|
||||
</div>
|
||||
<div id="file-list"></div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-check-circle"></i> Создать задачу
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="logs-section" class="section">
|
||||
<h2><i class="fas fa-history"></i> Лог активности</h2>
|
||||
<div id="logs-list"></div>
|
||||
</section>
|
||||
|
||||
<section id="profile-section" class="section">
|
||||
<h2><i class="fas fa-user-circle"></i> Личный кабинет</h2>
|
||||
|
||||
<div class="notification-settings">
|
||||
<h3><i class="fas fa-bell"></i> Настройки уведомлений</h3>
|
||||
<form id="notification-settings-form">
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="email-notifications" name="email_notifications">
|
||||
<span><i class="fas fa-envelope"></i> Email уведомления</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notification-email"><i class="fas fa-at"></i> Email для уведомлений:</label>
|
||||
<div class="input-with-icon">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<input type="email" id="notification-email" name="notification_email"
|
||||
placeholder="Введите email для уведомлений">
|
||||
</div>
|
||||
<small>Если не указано, будет использован email из профиля</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="telegram-notifications" name="telegram_notifications" disabled>
|
||||
<span><i class="fab fa-telegram"></i> Telegram уведомления (скоро)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="vk-notifications" name="vk_notifications" disabled>
|
||||
<span><i class="fab fa-vk"></i> ВКонтакте уведомления (скоро)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group"><label class="checkbox-label">
|
||||
<input type="checkbox" id="sberbank-notifications" name="sberbank_notifications" disabled>
|
||||
<span><i class="fas fa-university"></i> Сбербанк Онлайн уведомления (скоро)</span>
|
||||
</label></div>
|
||||
<div class="form-group"><label class="checkbox-label">
|
||||
<input type="checkbox" id="yandex-notifications" name="yandex_notifications" disabled>
|
||||
<span><i class="fab fa-yandex"></i> Яндекс уведомления (скоро)</span>
|
||||
</label></div>
|
||||
<div class="form-group"><label class="checkbox-label">
|
||||
<input type="checkbox" id="gosuslugi-notifications" name="gosuslugi_notifications" disabled>
|
||||
<span><i class="fas fa-passport"></i> Госуслуги уведомления (скоро)</span>
|
||||
</label></div>
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-save"></i> Сохранить настройки
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Модальные окна остаются без изменений -->
|
||||
<div id="edit-task-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeEditModal()">×</span>
|
||||
<h3><i class="fas fa-edit"></i> Редактировать задачу</h3>
|
||||
<form id="edit-task-form" enctype="multipart/form-data">
|
||||
<input type="hidden" id="edit-task-id">
|
||||
<div class="form-group">
|
||||
<label for="edit-title">Название задачи:</label>
|
||||
<input type="text" id="edit-title" name="title" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-description">Описание:</label>
|
||||
<textarea id="edit-description" name="description" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-due-date">Дата и время выполнения:</label>
|
||||
<input type="datetime-local" id="edit-due-date" name="dueDate" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Исполнители:</label>
|
||||
<div class="user-search">
|
||||
<input type="text" id="edit-user-search" placeholder="Поиск исполнителей..." oninput="filterEditUsers()">
|
||||
</div>
|
||||
<div id="edit-users-checklist" class="checkbox-group"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-files">Добавить файлы:</label>
|
||||
<input type="file" id="edit-files" name="files" multiple>
|
||||
<div id="edit-file-list"></div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-save"></i> Сохранить изменения
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="copy-task-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeCopyModal()">×</span>
|
||||
<h3><i class="fas fa-copy"></i> Создать копию задачи</h3>
|
||||
<form id="copy-task-form">
|
||||
<input type="hidden" id="copy-task-id">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="copy-due-date">Дата и время выполнения для копии:</label>
|
||||
<input type="datetime-local" id="copy-due-date" name="dueDate" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Назначить исполнителей для копии:</label>
|
||||
<div class="user-search">
|
||||
<input type="text" id="copy-user-search" placeholder="Поиск исполнителей..." oninput="filterCopyUsers()">
|
||||
</div>
|
||||
<div id="copy-users-checklist" class="checkbox-group"></div>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-copy"></i> Создать копию
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="edit-assignment-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeEditAssignmentModal()">×</span>
|
||||
<h3><i class="fas fa-clock"></i> Редактировать сроки исполнителя</h3>
|
||||
<form id="edit-assignment-form">
|
||||
<input type="hidden" id="edit-assignment-task-id">
|
||||
<input type="hidden" id="edit-assignment-user-id">
|
||||
<div class="form-group">
|
||||
<label for="edit-assignment-due-date">Дата и время выполнения:</label>
|
||||
<input type="datetime-local" id="edit-assignment-due-date" name="dueDate" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-save"></i> Сохранить сроки
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rework-task-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeReworkModal()">×</span>
|
||||
<h3><i class="fas fa-redo"></i> Вернуть задачу на доработку</h3>
|
||||
<form id="rework-task-form">
|
||||
<input type="hidden" id="rework-task-id">
|
||||
<div class="form-group">
|
||||
<label for="rework-comment">Комментарий к доработке:</label>
|
||||
<textarea id="rework-comment" name="comment" rows="4" placeholder="Укажите, что нужно исправить..." required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn-warning">
|
||||
<i class="fas fa-redo"></i> Вернуть на доработку
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kanban-section" class="section kanban-section">
|
||||
<div class="section-header">
|
||||
<h2><i class="fas fa-columns"></i> Канбан-доска</h2>
|
||||
<p>Перетаскивайте задачи между колонками для изменения статуса</p>
|
||||
<div class="kanban-controls">
|
||||
<div class="kanban-filters">
|
||||
<select id="kanban-filter" onchange="loadKanbanBoard()">
|
||||
<option value="all">Все задачи</option>
|
||||
<option value="created">Мои задачи (я создал)</option>
|
||||
<option value="assigned">Назначенные мне</option>
|
||||
</select>
|
||||
<select id="kanban-days" onchange="loadKanbanBoard()">
|
||||
<option value="7">7 дней</option>
|
||||
<option value="14">14 дней</option>
|
||||
<option value="30">30 дней</option>
|
||||
<option value="365">Все задачи</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kanban-board" class="kanban-board">
|
||||
<div class="loading">Загрузка Канбан-доски...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="auth.js"></script>
|
||||
<script src="users.js"></script>
|
||||
<script src="tasks.js"></script>
|
||||
<script src="kanban.js"></script>
|
||||
<script src="files.js"></script>
|
||||
<script src="profile.js"></script>
|
||||
<script src="ui.js"></script>
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
4556
public/style.css
4556
public/style.css
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user