diff --git a/database.js b/database.js
index 02ca5db..fc47a90 100644
--- a/database.js
+++ b/database.js
@@ -174,9 +174,176 @@ function createSQLiteTables() {
notification_key TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
-
+
console.log('✅ База данных SQLite инициализирована');
- setTimeout(addMissingColumns, 1000);
+
+ // Добавляем таблицу для пользовательских настроек
+ db.run(`CREATE TABLE IF NOT EXISTS user_settings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER UNIQUE NOT NULL,
+ email_notifications BOOLEAN DEFAULT true,
+ notification_email TEXT,
+ telegram_notifications BOOLEAN DEFAULT false,
+ telegram_chat_id TEXT,
+ vk_notifications BOOLEAN DEFAULT false,
+ vk_user_id TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users (id)
+ )`);
+
+ console.log('✅ Таблица для пользовательских настроек инициализирована');
+
+ // Запускаем проверку и обновление структуры таблиц
+ setTimeout(() => {
+ checkAndUpdateTableStructure();
+ }, 2000);
+}
+
+// Функция для проверки и обновления структуры таблиц
+function checkAndUpdateTableStructure() {
+ console.log('🔍 Проверка структуры таблиц...');
+
+ // Определяем ожидаемую структуру таблиц
+ const tableSchemas = {
+ users: [
+ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
+ { name: 'login', type: 'TEXT UNIQUE NOT NULL' },
+ { name: 'password', type: 'TEXT' },
+ { name: 'name', type: 'TEXT NOT NULL' },
+ { name: 'email', type: 'TEXT UNIQUE NOT NULL' },
+ { name: 'role', type: 'TEXT DEFAULT "teacher"' },
+ { name: 'auth_type', type: 'TEXT DEFAULT "local"' },
+ { name: 'groups', type: 'TEXT' },
+ { name: 'description', type: 'TEXT' },
+ { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
+ { name: 'last_login', type: 'DATETIME' },
+ { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
+ ],
+ tasks: [
+ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
+ { name: 'title', type: 'TEXT NOT NULL' },
+ { name: 'description', type: 'TEXT' },
+ { name: 'status', type: 'TEXT DEFAULT "active"' },
+ { name: 'created_by', type: 'INTEGER NOT NULL' },
+ { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
+ { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
+ { name: 'deleted_at', type: 'DATETIME' },
+ { name: 'deleted_by', type: 'INTEGER' },
+ { name: 'original_task_id', type: 'INTEGER' },
+ { name: 'start_date', type: 'DATETIME' },
+ { name: 'due_date', type: 'DATETIME' },
+ { name: 'rework_comment', type: 'TEXT' },
+ { name: 'closed_at', type: 'DATETIME' },
+ { name: 'closed_by', type: 'INTEGER' }
+ ],
+ task_assignments: [
+ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
+ { name: 'task_id', type: 'INTEGER NOT NULL' },
+ { name: 'user_id', type: 'INTEGER NOT NULL' },
+ { name: 'status', type: 'TEXT DEFAULT "assigned"' },
+ { name: 'start_date', type: 'DATETIME' },
+ { name: 'due_date', type: 'DATETIME' },
+ { name: 'rework_comment', type: 'TEXT' },
+ { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
+ { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
+ ],
+ task_files: [
+ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
+ { name: 'task_id', type: 'INTEGER NOT NULL' },
+ { name: 'user_id', type: 'INTEGER NOT NULL' },
+ { name: 'filename', type: 'TEXT NOT NULL' },
+ { name: 'original_name', type: 'TEXT NOT NULL' },
+ { name: 'file_path', type: 'TEXT NOT NULL' },
+ { name: 'file_size', type: 'INTEGER NOT NULL' },
+ { name: 'uploaded_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
+ ],
+ activity_logs: [
+ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
+ { name: 'task_id', type: 'INTEGER NOT NULL' },
+ { name: 'user_id', type: 'INTEGER NOT NULL' },
+ { name: 'action', type: 'TEXT NOT NULL' },
+ { name: 'details', type: 'TEXT' },
+ { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
+ ],
+ notification_logs: [
+ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
+ { name: 'notification_key', type: 'TEXT NOT NULL' },
+ { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
+ ],
+ user_settings: [
+ { name: 'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT' },
+ { name: 'user_id', type: 'INTEGER UNIQUE NOT NULL' },
+ { name: 'email_notifications', type: 'BOOLEAN DEFAULT true' },
+ { name: 'notification_email', type: 'TEXT' },
+ { name: 'telegram_notifications', type: 'BOOLEAN DEFAULT false' },
+ { name: 'telegram_chat_id', type: 'TEXT' },
+ { name: 'vk_notifications', type: 'BOOLEAN DEFAULT false' },
+ { name: 'vk_user_id', type: 'TEXT' },
+ { name: 'created_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' },
+ { name: 'updated_at', type: 'DATETIME DEFAULT CURRENT_TIMESTAMP' }
+ ]
+ };
+
+ // Проверяем каждую таблицу
+ Object.entries(tableSchemas).forEach(([tableName, columns]) => {
+ db.all(`PRAGMA table_info(${tableName})`, (err, existingColumns) => {
+ if (err) {
+ console.error(`❌ Ошибка проверки таблицы ${tableName}:`, err.message);
+ return;
+ }
+
+ if (existingColumns.length === 0) {
+ console.log(`⚠️ Таблица ${tableName} не существует, создаем...`);
+ // Таблица будет создана автоматически при следующем запуске
+ return;
+ }
+
+ // Создаем массив имен существующих колонок
+ const existingColumnNames = existingColumns.map(col => col.name.toLowerCase());
+
+ // Проверяем каждую ожидаемую колонку
+ columns.forEach(expectedColumn => {
+ const expectedName = expectedColumn.name.toLowerCase();
+
+ if (!existingColumnNames.includes(expectedName)) {
+ console.log(`🔧 Добавляем колонку ${expectedColumn.name} в таблицу ${tableName}...`);
+
+ db.run(
+ `ALTER TABLE ${tableName} ADD COLUMN ${expectedColumn.name} ${expectedColumn.type}`,
+ (alterErr) => {
+ if (alterErr) {
+ console.error(`❌ Ошибка добавления колонки ${expectedColumn.name}:`, alterErr.message);
+ } else {
+ console.log(`✅ Колонка ${expectedColumn.name} добавлена в таблицу ${tableName}`);
+ }
+ }
+ );
+ }
+ });
+ });
+ });
+
+ // Проверяем индекс для таблицы user_settings
+ setTimeout(() => {
+ db.get("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_user_settings_user_id'", (err, index) => {
+ if (err) {
+ console.error('❌ Ошибка проверки индекса:', err.message);
+ return;
+ }
+
+ if (!index) {
+ console.log('🔧 Создаем индекс для таблицы user_settings...');
+ db.run("CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)", (createErr) => {
+ if (createErr) {
+ console.error('❌ Ошибка создания индекса:', createErr.message);
+ } else {
+ console.log('✅ Индекс для user_settings создан');
+ }
+ });
+ }
+ });
+ }, 1000);
}
function createPostgresAdapter(pool) {
@@ -400,6 +567,22 @@ async function createPostgresTables() {
)
`);
+ // Добавляем таблицу для пользовательских настроек
+ await client.query(`
+ CREATE TABLE IF NOT EXISTS user_settings (
+ id SERIAL PRIMARY KEY,
+ user_id INTEGER UNIQUE NOT NULL REFERENCES users(id),
+ email_notifications BOOLEAN DEFAULT true,
+ notification_email TEXT,
+ telegram_notifications BOOLEAN DEFAULT false,
+ telegram_chat_id TEXT,
+ vk_notifications BOOLEAN DEFAULT false,
+ vk_user_id TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ `);
+
// Создаем индексы
const indexes = [
'CREATE INDEX IF NOT EXISTS idx_tasks_created_by ON tasks(created_by)',
@@ -410,7 +593,8 @@ async function createPostgresTables() {
'CREATE INDEX IF NOT EXISTS idx_task_assignments_status ON task_assignments(status)',
'CREATE INDEX IF NOT EXISTS idx_task_files_task_id ON task_files(task_id)',
'CREATE INDEX IF NOT EXISTS idx_activity_logs_task_id ON activity_logs(task_id)',
- 'CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at)'
+ 'CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at)',
+ 'CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)'
];
for (const indexQuery of indexes) {
@@ -424,40 +608,91 @@ async function createPostgresTables() {
client.release();
console.log('✅ Таблицы PostgreSQL проверены/созданы');
+ // Проверяем структуру PostgreSQL таблиц
+ await checkPostgresTableStructure();
+
} catch (error) {
console.error('❌ Ошибка создания таблиц PostgreSQL:', error.message);
}
}
-function addMissingColumns() {
- const columnsToAdd = [
- { table: 'tasks', column: 'rework_comment', type: 'TEXT' },
- { table: 'tasks', column: 'closed_at', type: 'DATETIME' },
- { table: 'tasks', column: 'closed_by', type: 'INTEGER' },
- { table: 'task_assignments', column: 'rework_comment', type: 'TEXT' }
- ];
-
- columnsToAdd.forEach(({ table, column, type }) => {
- db.all(`PRAGMA table_info(${table})`, (err, rows) => {
- if (err) {
- console.error(`Ошибка при проверке таблицы ${table}:`, err);
- return;
- }
-
- const columnExists = rows.some(row => row.name === column);
- if (!columnExists) {
- db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`, (err) => {
- if (err) {
- console.error(`Ошибка при добавлении колонки ${column} в таблицу ${table}:`, err);
- } else {
- console.log(`✅ Добавлена колонка ${column} в таблицу ${table}`);
+// Функция для проверки структуры таблиц PostgreSQL
+async function checkPostgresTableStructure() {
+ if (!USE_POSTGRES) return;
+
+ try {
+ const client = await postgresPool.connect();
+
+ console.log('🔍 Проверка структуры таблиц PostgreSQL...');
+
+ // Определяем ожидаемую структуру таблиц PostgreSQL
+ const tableSchemas = {
+ user_settings: [
+ { name: 'id', type: 'SERIAL PRIMARY KEY' },
+ { name: 'user_id', type: 'INTEGER UNIQUE NOT NULL REFERENCES users(id)' },
+ { name: 'email_notifications', type: 'BOOLEAN DEFAULT true' },
+ { name: 'notification_email', type: 'TEXT' },
+ { name: 'telegram_notifications', type: 'BOOLEAN DEFAULT false' },
+ { name: 'telegram_chat_id', type: 'TEXT' },
+ { name: 'vk_notifications', type: 'BOOLEAN DEFAULT false' },
+ { name: 'vk_user_id', type: 'TEXT' },
+ { name: 'created_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },
+ { name: 'updated_at', type: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' }
+ ]
+ };
+
+ // Проверяем каждую таблицу
+ for (const [tableName, columns] of Object.entries(tableSchemas)) {
+ try {
+ // Проверяем существование таблицы
+ const tableExists = await client.query(
+ "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1)",
+ [tableName]
+ );
+
+ if (!tableExists.rows[0].exists) {
+ console.log(`⚠️ Таблица ${tableName} не существует в PostgreSQL`);
+ continue;
+ }
+
+ // Получаем существующие колонки
+ const existingColumns = await client.query(`
+ SELECT column_name, data_type, is_nullable, column_default
+ FROM information_schema.columns
+ WHERE table_name = $1
+ ORDER BY ordinal_position
+ `, [tableName]);
+
+ const existingColumnNames = existingColumns.rows.map(col => col.column_name.toLowerCase());
+
+ // Проверяем каждую ожидаемую колонку
+ for (const expectedColumn of columns) {
+ const expectedName = expectedColumn.name.toLowerCase();
+
+ if (!existingColumnNames.includes(expectedName)) {
+ console.log(`🔧 Добавляем колонку ${expectedColumn.name} в таблицу PostgreSQL ${tableName}...`);
+
+ try {
+ await client.query(
+ `ALTER TABLE ${tableName} ADD COLUMN ${expectedColumn.name} ${expectedColumn.type}`
+ );
+ console.log(`✅ Колонка ${expectedColumn.name} добавлена в таблицу PostgreSQL ${tableName}`);
+ } catch (alterErr) {
+ console.error(`❌ Ошибка добавления колонки ${expectedColumn.name}:`, alterErr.message);
+ }
}
- });
- } else {
- console.log(`✅ Колонка ${column} уже существует в таблице ${table}`);
+ }
+ } catch (error) {
+ console.error(`❌ Ошибка проверки таблицы PostgreSQL ${tableName}:`, error.message);
}
- });
- });
+ }
+
+ client.release();
+ console.log('✅ Проверка структуры таблиц PostgreSQL завершена');
+
+ } catch (error) {
+ console.error('❌ Ошибка проверки структуры таблиц PostgreSQL:', error.message);
+ }
}
function createTaskFolder(taskId) {
@@ -598,7 +833,8 @@ module.exports = {
updateTaskMetadata,
checkTaskAccess,
USE_POSTGRES,
- getDatabaseType: () => USE_POSTGRES ? 'PostgreSQL' : 'SQLite'
+ getDatabaseType: () => USE_POSTGRES ? 'PostgreSQL' : 'SQLite',
+ checkAndUpdateTableStructure // Экспортируем для ручного запуска
};
// Запускаем инициализацию при экспорте (но она завершится позже)
diff --git a/email-notifications.js b/email-notifications.js
new file mode 100644
index 0000000..374f8d6
--- /dev/null
+++ b/email-notifications.js
@@ -0,0 +1,525 @@
+// email-notifications.js
+const nodemailer = require('nodemailer');
+const { getDb } = require('./database');
+
+class EmailNotifications {
+ constructor() {
+ this.transporter = null;
+ this.initialized = false;
+ this.init();
+ }
+
+ async init() {
+ try {
+ console.log('🔧 Инициализация Email уведомлений...');
+
+ if (!process.env.YANDEX_EMAIL || !process.env.YANDEX_PASSWORD) {
+ console.warn('⚠️ Настройки Яндекс почты не указаны в .env');
+ console.warn(' Email уведомления будут отключены');
+ this.initialized = false;
+ return;
+ }
+
+ this.transporter = nodemailer.createTransport({
+ host: process.env.YANDEX_SMTP_HOST || 'smtp.yandex.ru',
+ port: parseInt(process.env.YANDEX_SMTP_PORT) || 587,
+ secure: process.env.YANDEX_SMTP_SECURE === 'true',
+ auth: {
+ user: process.env.YANDEX_EMAIL,
+ pass: process.env.YANDEX_PASSWORD
+ },
+ tls: {
+ rejectUnauthorized: false
+ }
+ });
+
+ // Тестируем подключение
+ await this.transporter.verify();
+ this.initialized = true;
+ console.log('✅ Email уведомления инициализированы');
+ console.log(`📧 Отправитель: ${process.env.YANDEX_EMAIL}`);
+
+ } catch (error) {
+ console.error('❌ Ошибка инициализации Email уведомлений:', error.message);
+ this.initialized = false;
+ }
+ }
+
+ async getUserNotificationSettings(userId) {
+ if (!getDb) return null;
+
+ return new Promise((resolve, reject) => {
+ const db = getDb();
+ db.get(`
+ SELECT us.*, u.email as user_email, u.name as user_name
+ FROM user_settings us
+ LEFT JOIN users u ON us.user_id = u.id
+ WHERE us.user_id = ?
+ `, [userId], (err, settings) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(settings);
+ }
+ });
+ });
+ }
+
+ async saveUserNotificationSettings(userId, settings) {
+ if (!getDb) return false;
+
+ return new Promise((resolve, reject) => {
+ const db = getDb();
+ const {
+ email_notifications = true,
+ notification_email = '',
+ telegram_notifications = false,
+ telegram_chat_id = '',
+ vk_notifications = false,
+ vk_user_id = ''
+ } = settings;
+
+ // Проверяем существование записи
+ db.get("SELECT id FROM user_settings WHERE user_id = ?", [userId], (err, existing) => {
+ if (err) {
+ reject(err);
+ return;
+ }
+
+ if (existing) {
+ // Обновляем существующую запись
+ db.run(`
+ UPDATE user_settings
+ SET email_notifications = ?,
+ notification_email = ?,
+ telegram_notifications = ?,
+ telegram_chat_id = ?,
+ vk_notifications = ?,
+ vk_user_id = ?,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE user_id = ?
+ `, [
+ email_notifications ? 1 : 0,
+ notification_email,
+ telegram_notifications ? 1 : 0,
+ telegram_chat_id,
+ vk_notifications ? 1 : 0,
+ vk_user_id,
+ userId
+ ], function(err) {
+ if (err) reject(err);
+ else resolve(true);
+ });
+ } else {
+ // Создаем новую запись
+ db.run(`
+ INSERT INTO user_settings (
+ user_id, email_notifications, notification_email,
+ telegram_notifications, telegram_chat_id,
+ vk_notifications, vk_user_id
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
+ `, [
+ userId,
+ email_notifications ? 1 : 0,
+ notification_email,
+ telegram_notifications ? 1 : 0,
+ telegram_chat_id,
+ vk_notifications ? 1 : 0,
+ vk_user_id
+ ], function(err) {
+ if (err) reject(err);
+ else resolve(true);
+ });
+ }
+ });
+ });
+ }
+
+ async sendEmailNotification(to, subject, htmlContent) {
+ if (!this.initialized || !this.transporter) {
+ console.warn('⚠️ Email уведомления отключены');
+ return false;
+ }
+
+ try {
+ const info = await this.transporter.sendMail({
+ from: `"School CRM" <${process.env.YANDEX_EMAIL}>`,
+ to: to,
+ subject: subject,
+ html: htmlContent,
+ text: htmlContent.replace(/<[^>]*>/g, '') // Конвертируем HTML в текст
+ });
+
+ console.log(`📧 Email отправлен: ${to}, Message ID: ${info.messageId}`);
+ return true;
+
+ } catch (error) {
+ console.error('❌ Ошибка отправки email:', error.message);
+ return false;
+ }
+ }
+
+ async sendTaskNotification(userId, taskData, notificationType) {
+ try {
+ const settings = await this.getUserNotificationSettings(userId);
+ if (!settings || !settings.email_notifications) {
+ return false;
+ }
+
+ // Используем указанную email или email из профиля пользователя
+ const emailTo = settings.notification_email || settings.user_email;
+ if (!emailTo) {
+ console.log(`⚠️ У пользователя ${userId} не указан email для уведомлений`);
+ return false;
+ }
+
+ let subject = '';
+ let htmlContent = '';
+
+ switch (notificationType) {
+ case 'created':
+ subject = `Новая задача: ${taskData.title}`;
+ htmlContent = this.getTaskCreatedHtml(taskData);
+ break;
+ case 'updated':
+ subject = `Обновлена задача: ${taskData.title}`;
+ htmlContent = this.getTaskUpdatedHtml(taskData);
+ break;
+ case 'rework':
+ subject = `Задача возвращена на доработку: ${taskData.title}`;
+ htmlContent = this.getTaskReworkHtml(taskData);
+ break;
+ case 'closed':
+ subject = `Задача закрыта: ${taskData.title}`;
+ htmlContent = this.getTaskClosedHtml(taskData);
+ break;
+ case 'status_changed':
+ subject = `Изменен статус задачи: ${taskData.title}`;
+ htmlContent = this.getStatusChangedHtml(taskData);
+ break;
+ case 'deadline':
+ subject = `Скоро срок выполнения: ${taskData.title}`;
+ htmlContent = this.getDeadlineHtml(taskData);
+ break;
+ default:
+ subject = `Уведомление по задаче: ${taskData.title}`;
+ htmlContent = this.getDefaultHtml(taskData);
+ }
+
+ return await this.sendEmailNotification(emailTo, subject, htmlContent);
+
+ } catch (error) {
+ console.error('❌ Ошибка отправки уведомления о задаче:', error);
+ return false;
+ }
+ }
+
+ // HTML шаблоны для разных типов уведомлений
+ getTaskCreatedHtml(taskData) {
+ return `
+
+
+
+
+
+
+
+
+
+
${taskData.title}
+
+
Описание: ${taskData.description || 'Без описания'}
+
Срок выполнения: ${new Date(taskData.due_date).toLocaleString('ru-RU')}
+
Создал: ${taskData.author_name || 'Неизвестно'}
+
+
Для просмотра подробной информации перейдите в систему управления задачами.
+
Перейти в CRM
+
+
+
+
+
+ `;
+ }
+
+ getTaskUpdatedHtml(taskData) {
+ return `
+
+
+
+
+
+
+
+
+
+
${taskData.title}
+
+
Изменения внес: ${taskData.author_name || 'Неизвестно'}
+
Время: ${new Date().toLocaleString('ru-RU')}
+
+
Для просмотра изменений перейдите в систему управления задачами.
+
Перейти в CRM
+
+
+
+
+
+ `;
+ }
+
+ getTaskReworkHtml(taskData) {
+ return `
+
+
+
+
+
+
+
+
+
+
${taskData.title}
+
+
Автор замечания: ${taskData.author_name || 'Неизвестно'}
+
+
+
Пожалуйста, исправьте замечания и обновите статус задачи.
+
Перейти к задаче
+
+
+
+
+
+ `;
+ }
+
+ getTaskClosedHtml(taskData) {
+ return `
+
+
+
+
+
+
+
+
+
+
${taskData.title}
+
+
Закрыта: ${taskData.author_name || 'Неизвестно'}
+
Время закрытия: ${new Date().toLocaleString('ru-RU')}
+
+
Задача завершена и перемещена в архив.
+
Перейти в CRM
+
+
+
+
+
+ `;
+ }
+
+ getStatusChangedHtml(taskData) {
+ return `
+
+
+
+
+
+
+
+
+
+
${taskData.title}
+
+
Новый статус: ${this.getStatusText(taskData.status)}
+
Изменил: ${taskData.user_name || taskData.author_name || 'Неизвестно'}
+
Время: ${new Date().toLocaleString('ru-RU')}
+
+
Для просмотра деталей перейдите в систему управления задачами.
+
Перейти в CRM
+
+
+
+
+
+ `;
+ }
+
+ getDeadlineHtml(taskData) {
+ return `
+
+
+
+
+
+
+
+
+
+
${taskData.title}
+
+
ВНИМАНИЕ! До окончания срока задачи осталось менее ${taskData.hours_left} часов!
+
Срок выполнения: ${new Date(taskData.due_date).toLocaleString('ru-RU')}
+
+
Пожалуйста, завершите задачу в указанный срок.
+
Перейти к задаче
+
+
+
+
+
+ `;
+ }
+
+ getStatusText(status) {
+ const statusMap = {
+ 'assigned': 'Назначена',
+ 'in_progress': 'В работе',
+ 'completed': 'Завершена',
+ 'overdue': 'Просрочена',
+ 'rework': 'На доработке'
+ };
+ return statusMap[status] || status;
+ }
+
+ getDefaultHtml(taskData) {
+ return `
+
+
+
+
+
+
+
+
+
+
${taskData.title}
+
+
${taskData.message || 'Новое уведомление по задаче'}
+
+
Перейти в CRM
+
+
+
+
+
+ `;
+ }
+
+ isReady() {
+ return this.initialized;
+ }
+}
+
+// Singleton
+const emailNotifications = new EmailNotifications();
+module.exports = emailNotifications;
\ No newline at end of file
diff --git a/notifications.js b/notifications.js
index d17f857..124b7a7 100644
--- a/notifications.js
+++ b/notifications.js
@@ -1,97 +1,7 @@
-const fetch = require('node-fetch');
+// notifications.js
const postgresLogger = require('./postgres');
const { getDb } = require('./database');
-
-async function sendDeadlineNotification(assignment, hoursLeft) {
- try {
- if (!process.env.NOTIFICATION_SERVICE_URL ||
- !process.env.NOTIFICATION_SERVICE_LOGIN ||
- !process.env.NOTIFICATION_SERVICE_PASSWORD) {
- return;
- }
-
- const notificationKey = `deadline_${hoursLeft}h_task_${assignment.task_id}_user_${assignment.user_id}`;
- const lastSent = await getLastNotificationSent(notificationKey);
- const now = new Date();
-
- if (lastSent) {
- const timeSinceLast = now.getTime() - new Date(lastSent).getTime();
- if (timeSinceLast < 12 * 60 * 60 * 1000) {
- return;
- }
- }
-
- const subject = `⚠️ До окончания срока задачи осталось менее ${hoursLeft} часов`;
- const content = `Задача: ${assignment.title}\n\n` +
- `Описание: ${assignment.description || 'Без описания'}\n` +
- `Срок выполнения: ${new Date(assignment.due_date).toLocaleString('ru-RU')}\n` +
- `Осталось времени: ${hoursLeft} часов\n\n` +
- `Пожалуйста, завершите задачу в срок.`;
-
- const recipients = [
- { id: assignment.user_id, name: assignment.user_name, email: assignment.user_email },
- { id: assignment.created_by, name: assignment.creator_name, email: assignment.creator_email }
- ].filter((value, index, self) =>
- self.findIndex(r => r.id === value.id) === index
- );
-
- const recipientIds = recipients.map(r => r.id);
-
- const authHeader = encodeBasicAuth(
- process.env.NOTIFICATION_SERVICE_LOGIN,
- process.env.NOTIFICATION_SERVICE_PASSWORD
- );
-
- const FormData = require('form-data');
- const formData = new FormData();
- formData.append('subject', subject);
- formData.append('content', content);
- formData.append('recipients', JSON.stringify(recipientIds));
- formData.append('deliveryMethods', JSON.stringify(['email', 'telegram', 'vk']));
-
- const response = await fetch(process.env.NOTIFICATION_SERVICE_URL, {
- method: 'POST',
- headers: {
- 'Authorization': `Basic ${authHeader}`
- },
- body: formData
- });
-
- if (response.ok) {
- await saveNotificationSent(notificationKey);
- console.log(`✅ Уведомление о сроке (${hoursLeft}ч) отправлено для задачи ${assignment.task_id}`);
- }
- } catch (error) {
- console.error('❌ Ошибка отправки уведомления о сроке:', error);
- }
-}
-
-function getLastNotificationSent(key) {
- return new Promise((resolve) => {
- const db = getDb();
- if (!db) {
- resolve(null);
- return;
- }
-
- db.get("SELECT created_at FROM notification_logs WHERE notification_key = ? ORDER BY created_at DESC LIMIT 1",
- [key], (err, row) => {
- resolve(row ? row.created_at : null);
- }
- );
- });
-}
-
-function saveNotificationSent(key) {
- const db = getDb();
- if (!db) return;
-
- db.run("INSERT INTO notification_logs (notification_key) VALUES (?)", [key]);
-}
-
-function encodeBasicAuth(login, password) {
- return Buffer.from(`${login}:${password}`).toString('base64');
-}
+const emailNotifications = require('./email-notifications');
async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, authorId, comment = '', status = '', userName = '') {
try {
@@ -101,31 +11,10 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a
return;
}
- if (!process.env.NOTIFICATION_SERVICE_URL ||
- !process.env.NOTIFICATION_SERVICE_LOGIN ||
- !process.env.NOTIFICATION_SERVICE_PASSWORD) {
- console.log('⚠️ Настройки сервиса уведомлений не заданы');
-
- // Логируем в PostgreSQL даже если уведомления не отправляются
- await logNotificationToPostgres({
- type,
- taskId,
- taskTitle,
- taskDescription,
- authorId,
- comment,
- status,
- userName,
- error: 'Сервис уведомлений не настроен'
- });
-
- return;
- }
-
console.log(`📢 Начинаем отправку уведомлений для задачи ${taskId}:`);
console.log(` Тип: ${type}, Автор действия: ${authorId}, Название: ${taskTitle}`);
- // Получаем заказчика (создателя задачи) ОТДЕЛЬНО
+ // Получаем заказчика
const creator = await new Promise((resolve, reject) => {
db.get(`
SELECT t.created_by as user_id, u.name as user_name, u.login as user_login, u.email
@@ -138,7 +27,7 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a
});
});
- // Получаем исполнителей ОТДЕЛЬНО
+ // Получаем исполнителей
const assignees = await new Promise((resolve, reject) => {
db.all(`
SELECT ta.user_id, u.name as user_name, u.login as user_login, u.email
@@ -151,29 +40,9 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a
});
});
- // Собираем всех участников
- const participants = [];
- if (creator) {
- participants.push({
- ...creator,
- role: 'creator',
- is_creator: true
- });
- }
-
- if (assignees && assignees.length > 0) {
- assignees.forEach(assignee => {
- participants.push({
- ...assignee,
- role: 'assignee',
- is_creator: false
- });
- });
- }
-
// Получаем информацию об авторе действия
const author = await new Promise((resolve, reject) => {
- db.get("SELECT name, login FROM users WHERE id = ?", [authorId], (err, row) => {
+ db.get("SELECT name, login, email FROM users WHERE id = ?", [authorId], (err, row) => {
if (err) reject(err);
else resolve(row);
});
@@ -182,148 +51,134 @@ async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, a
const authorName = author ? author.name : 'Система';
const authorLogin = author ? author.login : 'system';
- // Логируем в PostgreSQL
- const postgresLogIds = await logNotificationToPostgres({
- type,
- taskId,
- taskTitle,
- taskDescription,
- authorId,
- authorName,
- authorLogin,
- participants,
- comment,
- status,
- userName
- });
-
- let subject, content;
-
- switch (type) {
- case 'created':
- subject = `Новая задача: ${taskTitle}`;
- content = `Создана новая задача:\n\n` +
- `📋 ${taskTitle}\n` +
- `📝 ${taskDescription || 'Без описания'}\n` +
- `👤 Автор: ${authorName}\n\n` +
- `Для просмотра перейдите в систему управления задачами.`;
- break;
-
- case 'updated':
- subject = `Обновлена задача: ${taskTitle}`;
- content = `Задача была обновлена:\n\n` +
- `📋 ${taskTitle}\n` +
- `📝 ${taskDescription || 'Без описания'}\n` +
- `👤 Изменено: ${authorName}\n\n` +
- `Для просмотра изменений перейдите в систему управления задачами.`;
- break;
-
- case 'rework':
- subject = `Задача возвращена на доработку: ${taskTitle}`;
- content = `Задача возвращена на доработку:\n\n` +
- `📋 ${taskTitle}\n` +
- `📝 Комментарий: ${comment}\n` +
- `👤 Автор замечания: ${authorName}\n\n` +
- `Пожалуйста, исправьте замечания и обновите статус задачи.`;
- break;
-
- case 'closed':
- subject = `Задача закрыта: ${taskTitle}`;
- content = `Задача была закрыта:\n\n` +
- `📋 ${taskTitle}\n` +
- `👤 Закрыта: ${authorName}\n\n` +
- `Задача завершена и перемещена в архив.`;
- break;
-
- case 'status_changed':
- const statusText = getStatusText(status);
- subject = `Изменен статус задачи: ${taskTitle}`;
- content = `Статус задачи изменен:\n\n` +
- `📋 ${taskTitle}\n` +
- `🔄 Новый статус: ${statusText}\n` +
- `👤 Изменил: ${userName || authorName}\n\n` +
- `Для просмотра перейдите в систему управления задачами.`;
- break;
-
- default:
- console.log(`⚠️ Неизвестный тип уведомления: ${type}`);
- return;
- }
-
- // Фильтруем получателей: исключаем автора действия
- const recipientIds = participants
- .filter(p => {
- const shouldExclude = p.user_id === authorId;
- if (shouldExclude) {
- console.log(` ✋ Исключаем автора действия: ${p.user_name} (ID: ${p.user_id})`);
- }
- return !shouldExclude;
- })
- .map(p => p.user_id);
-
- if (recipientIds.length === 0) {
- console.log('❌ Нет получателей для уведомления (все участники - автор изменения)');
-
- // Обновляем статус в PostgreSQL
- await updatePostgresLogStatus(postgresLogIds, 'no_recipients', 'Нет получателей после фильтрации');
- return;
- }
-
- const authHeader = encodeBasicAuth(
- process.env.NOTIFICATION_SERVICE_LOGIN,
- process.env.NOTIFICATION_SERVICE_PASSWORD
- );
-
- const FormData = require('form-data');
- const formData = new FormData();
- formData.append('subject', subject);
- formData.append('content', content);
- formData.append('recipients', JSON.stringify(recipientIds));
- formData.append('deliveryMethods', JSON.stringify(['email', 'telegram', 'vk']));
-
- console.log(`🚀 Отправляем запрос на сервис уведомлений...`);
-
- try {
- const response = await fetch(process.env.NOTIFICATION_SERVICE_URL, {
- method: 'POST',
- headers: {
- 'Authorization': `Basic ${authHeader}`
- },
- body: formData
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const result = await response.json();
- console.log(`✅ Уведомления успешно отправлены для задачи ${taskId}`);
-
- // Обновляем статус в PostgreSQL
- await updatePostgresLogStatus(postgresLogIds, 'sent', null, new Date().toISOString());
-
- console.log(` Результат от сервиса:`, result);
-
- } catch (error) {
- console.error('❌ Ошибка отправки уведомлений:', error);
-
- // Обновляем статус в PostgreSQL
- await updatePostgresLogStatus(postgresLogIds, 'failed', error.message);
-
- console.error(' Детали ошибки:', {
- taskId,
+ // Логируем в PostgreSQL (если настроено)
+ let postgresLogIds = [];
+ if (postgresLogger.initialized) {
+ postgresLogIds = await logNotificationToPostgres({
type,
+ taskId,
+ taskTitle,
+ taskDescription,
authorId,
- errorMessage: error.message,
- stack: error.stack
+ authorName,
+ authorLogin,
+ participants: [...(creator ? [{...creator, role: 'creator'}] : []), ...assignees.map(a => ({...a, role: 'assignee'}))],
+ comment,
+ status,
+ userName
});
}
+ // Отправляем email уведомления
+ const participants = [...(creator ? [creator] : []), ...assignees].filter(p => p.user_id !== authorId);
+
+ for (const participant of participants) {
+ const taskData = {
+ taskId,
+ title: taskTitle,
+ description: taskDescription,
+ due_date: null, // Можно добавить получение срока из БД
+ author_name: authorName,
+ comment: comment,
+ status: status,
+ user_name: userName || participant.user_name,
+ hours_left: type === 'deadline' ? 24 : null // Для уведомлений о дедлайне
+ };
+
+ await emailNotifications.sendTaskNotification(
+ participant.user_id,
+ taskData,
+ type
+ );
+ }
+
+ console.log(`✅ Уведомления отправлены для задачи ${taskId}`);
+
} catch (error) {
console.error('❌ Общая ошибка при обработке уведомлений:', error);
}
}
+// Обновим функцию для уведомлений о дедлайнах
+async function sendDeadlineNotification(assignment, hoursLeft) {
+ try {
+ const taskData = {
+ taskId: assignment.task_id,
+ title: assignment.title,
+ description: assignment.description || '',
+ due_date: assignment.due_date,
+ author_name: assignment.creator_name,
+ hours_left: hoursLeft
+ };
+
+ // Отправляем уведомление исполнителю
+ await emailNotifications.sendTaskNotification(
+ assignment.user_id,
+ taskData,
+ 'deadline'
+ );
+
+ // Отправляем уведомление заказчику
+ await emailNotifications.sendTaskNotification(
+ assignment.created_by,
+ taskData,
+ 'deadline'
+ );
+
+ console.log(`✅ Email уведомление о сроке (${hoursLeft}ч) отправлено для задачи ${assignment.task_id}`);
+
+ } catch (error) {
+ console.error('❌ Ошибка отправки email уведомления о сроке:', error);
+ }
+}
+
+async function checkUpcomingDeadlines() {
+ const now = new Date();
+ const in48Hours = new Date(now.getTime() + 48 * 60 * 60 * 1000).toISOString();
+ const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString();
+ const nowISO = now.toISOString();
+
+ const query = `
+ SELECT DISTINCT ta.*, t.title, t.description, t.created_by, u.name as user_name, u.email as user_email,
+ creator.name as creator_name, creator.email as creator_email
+ FROM task_assignments ta
+ JOIN tasks t ON ta.task_id = t.id
+ JOIN users u ON ta.user_id = u.id
+ JOIN users creator ON t.created_by = creator.id
+ WHERE ta.due_date IS NOT NULL
+ AND ta.due_date > ?
+ AND ta.due_date <= ?
+ AND ta.status NOT IN ('completed', 'overdue')
+ AND t.status = 'active'
+ AND t.closed_at IS NULL
+ `;
+
+ const db = getDb();
+ if (!db) {
+ console.error('❌ База данных не доступна для проверки сроков');
+ return;
+ }
+
+ db.all(query, [nowISO, in48Hours], async (err, assignments) => {
+ if (err) {
+ console.error('❌ Ошибка при проверке сроков задач:', err);
+ return;
+ }
+
+ for (const assignment of assignments) {
+ const dueDate = new Date(assignment.due_date);
+ const timeLeft = dueDate.getTime() - now.getTime();
+ const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000));
+
+ if (hoursLeft <= 48 && hoursLeft > 24) {
+ await sendDeadlineNotification(assignment, 48);
+ } else if (hoursLeft <= 24) {
+ await sendDeadlineNotification(assignment, 24);
+ }
+ }
+ });
+}
+
// Вспомогательные функции для работы с PostgreSQL
async function logNotificationToPostgres(data) {
try {
@@ -433,57 +288,11 @@ function getStatusText(status) {
return statusMap[status] || status;
}
-function checkUpcomingDeadlines() {
- const now = new Date();
- const in48Hours = new Date(now.getTime() + 48 * 60 * 60 * 1000).toISOString();
- const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString();
- const nowISO = now.toISOString();
-
- const query = `
- SELECT DISTINCT ta.*, t.title, t.description, t.created_by, u.name as user_name, u.email as user_email,
- creator.name as creator_name, creator.email as creator_email
- FROM task_assignments ta
- JOIN tasks t ON ta.task_id = t.id
- JOIN users u ON ta.user_id = u.id
- JOIN users creator ON t.created_by = creator.id
- WHERE ta.due_date IS NOT NULL
- AND ta.due_date > ?
- AND ta.due_date <= ?
- AND ta.status NOT IN ('completed', 'overdue')
- AND t.status = 'active'
- AND t.closed_at IS NULL
- `;
-
- const db = getDb();
- if (!db) {
- console.error('❌ База данных не доступна для проверки сроков');
- return;
- }
-
- db.all(query, [nowISO, in48Hours], async (err, assignments) => {
- if (err) {
- console.error('❌ Ошибка при проверке сроков задач:', err);
- return;
- }
-
- for (const assignment of assignments) {
- const dueDate = new Date(assignment.due_date);
- const timeLeft = dueDate.getTime() - now.getTime();
- const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000));
-
- if (hoursLeft <= 48 && hoursLeft > 24) {
- await sendDeadlineNotification(assignment, 48);
- } else if (hoursLeft <= 24) {
- await sendDeadlineNotification(assignment, 24);
- }
- }
- });
-}
-
// Экспортируем функции
module.exports = {
sendTaskNotifications,
checkUpcomingDeadlines,
sendDeadlineNotification,
- getStatusText
+ getStatusText,
+ emailNotifications // Экспортируем для доступа к методам
};
\ No newline at end of file
diff --git a/package.json b/package.json
index 4610258..0b22f85 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"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"
},
diff --git a/public/auth.js b/public/auth.js
new file mode 100644
index 0000000..6abd9c7
--- /dev/null
+++ b/public/auth.js
@@ -0,0 +1,97 @@
+// auth.js - Аутентификация и авторизация
+let currentUser = null;
+
+async function checkAuth() {
+ try {
+ const response = await fetch('/api/user');
+ if (response.ok) {
+ const data = await response.json();
+ currentUser = data.user;
+ showMainInterface();
+ } else {
+ showLoginInterface();
+ }
+ } catch (error) {
+ showLoginInterface();
+ }
+}
+
+function showLoginInterface() {
+ document.getElementById('login-modal').style.display = 'block';
+ document.querySelector('.container').style.display = 'none';
+}
+
+function showMainInterface() {
+ document.getElementById('login-modal').style.display = 'none';
+ document.querySelector('.container').style.display = 'block';
+
+ let userInfo = `Вы вошли как: ${currentUser.name}`;
+ if (currentUser.auth_type === 'ldap') {
+ userInfo += ` (LDAP)`;
+ }
+ if (currentUser.groups && currentUser.groups.length > 0) {
+ userInfo += ` | Группы: ${currentUser.groups.join(', ')}`;
+ }
+
+ document.getElementById('current-user').textContent = userInfo;
+
+ document.getElementById('tasks-controls').style.display = 'block';
+
+ const showDeletedLabel = document.querySelector('.show-deleted-label');
+ if (showDeletedLabel) {
+ if (currentUser.role === 'admin') {
+ showDeletedLabel.style.display = 'flex';
+ } else {
+ showDeletedLabel.style.display = 'none';
+ }
+ }
+
+ loadUsers();
+ loadTasks();
+ loadActivityLogs();
+ showSection('tasks');
+ loadKanbanTasks();
+
+ showingTasksWithoutDate = false;
+ const btn = document.getElementById('tasks-no-date-btn');
+ if (btn) btn.classList.remove('active');
+}
+
+async function login(event) {
+ event.preventDefault();
+
+ const login = document.getElementById('login').value;
+ const password = document.getElementById('password').value;
+
+ try {
+ const response = await fetch('/api/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ login, password })
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ currentUser = data.user;
+ showMainInterface();
+ } else {
+ const error = await response.json();
+ alert(error.error || 'Ошибка входа');
+ }
+ } catch (error) {
+ console.error('Ошибка:', error);
+ alert('Ошибка подключения к серверу');
+ }
+}
+
+async function logout() {
+ try {
+ await fetch('/api/logout', { method: 'POST' });
+ currentUser = null;
+ showLoginInterface();
+ } catch (error) {
+ console.error('Ошибка выхода:', error);
+ }
+}
\ No newline at end of file
diff --git a/public/files.js b/public/files.js
new file mode 100644
index 0000000..d37666a
--- /dev/null
+++ b/public/files.js
@@ -0,0 +1,451 @@
+// files.js - Работа с файлами
+let currentTaskFiles = [];
+let currentEditTaskFiles = [];
+
+function initializeFileUploads() {
+ // Создание задачи
+ document.getElementById('files').addEventListener('change', function(e) {
+ currentTaskFiles = Array.from(e.target.files);
+ updateFileList();
+ });
+
+ // Редактирование задачи
+ document.getElementById('edit-files').addEventListener('change', function(e) {
+ const newFiles = Array.from(e.target.files);
+ currentEditTaskFiles.push(...newFiles);
+ updateEditFileList();
+ });
+}
+
+function updateFileList() {
+ const fileInput = document.getElementById('files');
+ const fileList = document.getElementById('file-list');
+ updateFileListForInput(fileInput, fileList);
+}
+
+function updateEditFileList() {
+ const fileInput = document.getElementById('edit-files');
+ const fileList = document.getElementById('edit-file-list');
+
+ // Используем улучшенный рендеринг файлов
+ const files = fileInput.files;
+ const existingFiles = currentEditTaskFiles.filter(file => !(file instanceof File));
+
+ if (files.length === 0 && existingFiles.length === 0) {
+ fileList.innerHTML = '';
+ return;
+ }
+
+ let html = '';
+ let totalSize = 0;
+
+ // Существующие файлы
+ existingFiles.forEach(file => {
+ totalSize += file.file_size;
+ html += `- ${file.original_name} (${(file.file_size / 1024 / 1024).toFixed(2)} MB) - уже загружен
`;
+ });
+
+ // Новые файлы
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i];
+ totalSize += file.size;
+ html += `- ${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB) - новый
`;
+ }
+
+ html += '
';
+ html += `Общий размер: ${(totalSize / 1024 / 1024).toFixed(2)} MB / 300 MB
`;
+
+ fileList.innerHTML = html;
+}
+
+function updateFileListForInput(fileInput, fileList) {
+ const files = fileInput.files;
+
+ if (files.length === 0) {
+ fileList.innerHTML = '';
+ return;
+ }
+
+ let html = '';
+ let totalSize = 0;
+
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i];
+ totalSize += file.size;
+ html += `- ${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)
`;
+ }
+
+ html += '
';
+ html += `Общий размер: ${(totalSize / 1024 / 1024).toFixed(2)} MB / 300 MB
`;
+
+ fileList.innerHTML = html;
+}
+
+// Удаление файлов из списка
+function removeFile(index) {
+ currentTaskFiles.splice(index, 1);
+ updateFileList();
+
+ // Обновляем input files
+ const dataTransfer = new DataTransfer();
+ currentTaskFiles.forEach(file => dataTransfer.items.add(file));
+ document.getElementById('files').files = dataTransfer.files;
+}
+
+function removeEditFile(index) {
+ currentEditTaskFiles.splice(index, 1);
+ updateEditFileList();
+
+ // Обновляем input files
+ const dataTransfer = new DataTransfer();
+ const newFiles = currentEditTaskFiles.filter(file => file instanceof File);
+ newFiles.forEach(file => dataTransfer.items.add(file));
+ document.getElementById('edit-files').files = dataTransfer.files;
+}
+
+async function loadTaskFiles(taskId) {
+ try {
+ const response = await fetch(`/api/tasks/${taskId}/files`);
+ const files = await response.json();
+
+ const container = document.getElementById(`files-${taskId}`);
+ if (container) {
+ if (files.length === 0) {
+ container.innerHTML = 'Файлы: скрыто';
+ } else {
+ container.innerHTML = `
+ Файлы:
+
+ ${files.map(file => renderFileIcon(file)).join('')}
+
+ `;
+ }
+ }
+ } catch (error) {
+ console.error('Ошибка загрузки файлов:', error);
+ }
+}
+
+function renderFileIcon(file) {
+ // Исправляем кодировку имени файла
+ const fixEncoding = (str) => {
+ if (!str) return '';
+ try {
+ // Пробуем разные способы декодирования
+ if (str.includes('Ð') || str.includes('Ñ')) {
+ // UTF-8 неправильно декодированный как Latin-1
+ return decodeURIComponent(escape(str));
+ }
+ return str;
+ } catch (e) {
+ return str;
+ }
+ };
+
+ const fileName = fixEncoding(file.original_name);
+ const fileSize = (file.file_size / 1024 / 1024).toFixed(2);
+ const uploadedBy = file.user_name;
+
+ let iconColor = '';
+ let iconText = '';
+ let textClass = '';
+
+ // Определяем расширение файла
+ const extension = fileName.includes('.') ?
+ fileName.split('.').pop().toLowerCase() :
+ '';
+
+ // Определяем тип файла на основе расширения
+ if (extension) {
+ switch (extension) {
+ case 'pdf':
+ iconColor = '#e74c3c';
+ iconText = 'PDF';
+ textClass = 'short';
+ break;
+ case 'doc':
+ iconColor = '#3498db';
+ iconText = 'DOC';
+ textClass = 'short';
+ break;
+ case 'docx':
+ iconColor = '#3498db';
+ iconText = 'DOCX';
+ textClass = 'medium';
+ break;
+ case 'xls':
+ iconColor = '#2ecc71';
+ iconText = 'XLS';
+ textClass = 'short';
+ break;
+ case 'xlsx':
+ iconColor = '#2ecc71';
+ iconText = 'XLSX';
+ textClass = 'medium';
+ break;
+ case 'csv':
+ iconColor = '#2ecc71';
+ iconText = 'CSV';
+ textClass = 'short';
+ break;
+ case 'ppt':
+ iconColor = '#e67e22';
+ iconText = 'PPT';
+ textClass = 'short';
+ break;
+ case 'pptx':
+ iconColor = '#e67e22';
+ iconText = 'PPTX';
+ textClass = 'medium';
+ break;
+ case 'zip':
+ iconColor = '#f39c12';
+ iconText = 'ZIP';
+ textClass = 'short';
+ break;
+ case 'rar':
+ iconColor = '#f39c12';
+ iconText = 'RAR';
+ textClass = 'short';
+ break;
+ case '7z':
+ iconColor = '#f39c12';
+ iconText = '7Z';
+ textClass = 'short';
+ break;
+ case 'tar':
+ iconColor = '#f39c12';
+ iconText = 'TAR';
+ textClass = 'short';
+ break;
+ case 'gz':
+ iconColor = '#f39c12';
+ iconText = 'GZ';
+ textClass = 'short';
+ break;
+ case 'txt':
+ iconColor = '#95a5a6';
+ iconText = 'TXT';
+ textClass = 'short';
+ break;
+ case 'log':
+ iconColor = '#95a5a6';
+ iconText = 'LOG';
+ textClass = 'short';
+ break;
+ case 'md':
+ iconColor = '#95a5a6';
+ iconText = 'MD';
+ textClass = 'short';
+ break;
+ case 'jpg':
+ iconColor = '#9b59b6';
+ iconText = 'JPG';
+ textClass = 'short';
+ break;
+ case 'jpeg':
+ iconColor = '#9b59b6';
+ iconText = 'JPEG';
+ textClass = 'medium';
+ break;
+ case 'png':
+ iconColor = '#9b59b6';
+ iconText = 'PNG';
+ textClass = 'short';
+ break;
+ case 'gif':
+ iconColor = '#9b59b6';
+ iconText = 'GIF';
+ textClass = 'short';
+ break;
+ case 'bmp':
+ iconColor = '#9b59b6';
+ iconText = 'BMP';
+ textClass = 'short';
+ break;
+ case 'svg':
+ iconColor = '#9b59b6';
+ iconText = 'SVG';
+ textClass = 'short';
+ break;
+ case 'webp':
+ iconColor = '#9b59b6';
+ iconText = 'WEBP';
+ textClass = 'medium';
+ break;
+ case 'mp3':
+ iconColor = '#1abc9c';
+ iconText = 'MP3';
+ textClass = 'short';
+ break;
+ case 'wav':
+ iconColor = '#1abc9c';
+ iconText = 'WAV';
+ textClass = 'short';
+ break;
+ case 'ogg':
+ iconColor = '#1abc9c';
+ iconText = 'OGG';
+ textClass = 'short';
+ break;
+ case 'flac':
+ iconColor = '#1abc9c';
+ iconText = 'FLAC';
+ textClass = 'medium';
+ break;
+ case 'mp4':
+ iconColor = '#d35400';
+ iconText = 'MP4';
+ textClass = 'short';
+ break;
+ case 'avi':
+ iconColor = '#d35400';
+ iconText = 'AVI';
+ textClass = 'short';
+ break;
+ case 'mkv':
+ iconColor = '#d35400';
+ iconText = 'MKV';
+ textClass = 'short';
+ break;
+ case 'mov':
+ iconColor = '#d35400';
+ iconText = 'MOV';
+ textClass = 'short';
+ break;
+ case 'wmv':
+ iconColor = '#d35400';
+ iconText = 'WMV';
+ textClass = 'short';
+ break;
+ case 'exe':
+ iconColor = '#c0392b';
+ iconText = 'EXE';
+ textClass = 'short';
+ break;
+ case 'msi':
+ iconColor = '#c0392b';
+ iconText = 'MSI';
+ textClass = 'short';
+ break;
+ case 'js':
+ iconColor = '#2980b9';
+ iconText = 'JS';
+ textClass = 'short';
+ break;
+ case 'html':
+ iconColor = '#2980b9';
+ iconText = 'HTML';
+ textClass = 'medium';
+ break;
+ case 'css':
+ iconColor = '#2980b9';
+ iconText = 'CSS';
+ textClass = 'short';
+ break;
+ case 'php':
+ iconColor = '#2980b9';
+ iconText = 'PHP';
+ textClass = 'short';
+ break;
+ case 'py':
+ iconColor = '#2980b9';
+ iconText = 'PY';
+ textClass = 'short';
+ break;
+ case 'java':
+ iconColor = '#2980b9';
+ iconText = 'JAVA';
+ textClass = 'medium';
+ break;
+ case 'json':
+ iconColor = '#8e44ad';
+ iconText = 'JSON';
+ textClass = 'medium';
+ break;
+ case 'xml':
+ iconColor = '#8e44ad';
+ iconText = 'XML';
+ textClass = 'short';
+ break;
+ case 'yml':
+ iconColor = '#8e44ad';
+ iconText = 'YML';
+ textClass = 'short';
+ break;
+ case 'yaml':
+ iconColor = '#8e44ad';
+ iconText = 'YAML';
+ textClass = 'medium';
+ break;
+ case 'sql':
+ iconColor = '#27ae60';
+ iconText = 'SQL';
+ textClass = 'short';
+ break;
+ case 'db':
+ iconColor = '#27ae60';
+ iconText = 'DB';
+ textClass = 'short';
+ break;
+ case 'sqlite':
+ iconColor = '#27ae60';
+ iconText = 'SQLITE';
+ textClass = 'long';
+ break;
+ default:
+ // Для других расширений используем расширение или первые 4 символа
+ iconColor = '#7f8c8d';
+ iconText = extension.length > 4 ?
+ extension.substring(0, 4).toUpperCase() :
+ extension.toUpperCase();
+
+ // Определяем класс по длине текста
+ if (iconText.length <= 2) {
+ textClass = 'short';
+ } else if (iconText.length <= 4) {
+ textClass = 'medium';
+ } else {
+ textClass = 'long';
+ }
+ }
+ } else {
+ // Если нет расширения
+ iconColor = '#7f8c8d';
+ iconText = 'ФАЙЛ';
+ textClass = 'short';
+ }
+
+ // Исправляем кодировку для отображения
+ const safeFileName = fileName;
+ const displayFileName = truncateFileName(safeFileName);
+
+ return `
+
+
+ ${iconText}
+
+ ${displayFileName}
+
+ `;
+}
+
+function truncateFileName(fileName, maxLength = 20) {
+ if (fileName.length <= maxLength) return fileName;
+ const extension = fileName.split('.').pop();
+ const name = fileName.substring(0, fileName.lastIndexOf('.'));
+ const truncatedName = name.substring(0, maxLength - extension.length - 3) + '...';
+ return truncatedName + '.' + extension;
+}
+
+// Вспомогательная функция для форматирования размера файла
+function formatFileSize(bytes) {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index b47d7ca..0ef7809 100644
--- a/public/index.html
+++ b/public/index.html
@@ -5,26 +5,29 @@
School CRM - Управление задачами
+
-
Вход в School CRM
+
Вход в School CRM
-
Управление задачами
+
Управление задачами
- - @2025 МАОУ - СОШ № 25
+ - @2025 МАОУ - СОШ № 25
@@ -32,32 +35,53 @@
- School CRM - Управление задачами
-
-
-
+
- Все задачи
+ Все задачи
- Создать новую задачу
+ Создать новую задачу
- Лог активности
+ Лог активности
+
+
+
×
-
Редактировать задачу
+
Редактировать задачу
@@ -184,7 +273,7 @@
×
-
Создать копию задачи
+ Создать копию задачи
-
+
@@ -208,7 +299,7 @@
×
-
Редактировать сроки исполнителя
+ Редактировать сроки исполнителя
-
+
@@ -224,27 +317,53 @@
×
-
Вернуть задачу на доработку
+
Вернуть задачу на доработку
-
-
-
+
+
+
+
+
+
+
+
+
Комментарий:
+${taskData.comment || 'Требуется доработка'}
+