Загрузка Канбан-доски...
diff --git a/public/main.js b/public/main.js
index e2468ea..f9f4ae4 100644
--- a/public/main.js
+++ b/public/main.js
@@ -85,6 +85,12 @@ function setupEventListeners() {
notificationForm._hasSubmitListener = true;
}
+ // Ознакомление
+ const acquaintanceForm = document.getElementById('acquaintance-task-form');
+ if (acquaintanceForm && !acquaintanceForm._hasSubmitListener) {
+ acquaintanceForm.addEventListener('submit', createAcquaintanceTask);
+ acquaintanceForm._hasSubmitListener = true;
+ }
// Инициализация загрузки файлов
initializeFileUploads();
}
diff --git a/public/nav-task-actions.js b/public/nav-task-actions.js
index 7de4d56..2979ed1 100644
--- a/public/nav-task-actions.js
+++ b/public/nav-task-actions.js
@@ -131,7 +131,16 @@ if (currentUser && (currentUser.role === 'admin' || (currentUser.role === 'tasks
}
}
}
-
+
+ // Кнопка "Создать ознакомление" для админов и tasks
+ if (currentUser && (currentUser.role === 'admin' || currentUser.role === 'tasks')) {
+ actions.push({
+ label: '📖 Создать ознакомление',
+ handler: () => openAcquaintanceModal(task.id),
+ primary: true // можно отнести к админским или отдельной категории
+ });
+ }
+
// Доработка и изменение срока для необычных задач (исполнитель)
if (!isDeleted && !isClosed && task.task_type !== 'regular' && task.assignments && task.assignments.some(a => parseInt(a.user_id) === currentUser?.id)) {
if (typeof openReworkModal === 'function') { actions.push({ label: '🔄 Доработка', handler: () => openReworkModal(taskId),primary_task: true});
diff --git a/public/reports.js b/public/reports.js
index 5dbd66e..eca4ce1 100644
--- a/public/reports.js
+++ b/public/reports.js
@@ -27,7 +27,8 @@ const TASK_TYPE_OPTIONS = [
{ value: 'speech_therapist', label: 'Логопед' },
{ value: 'hr', label: 'Диспетчер расписания' },
{ value: 'certificate', label: 'Справка' },
- { value: 'e_journal', label: 'Эл. журнал' }
+ { value: 'e_journal', label: 'Эл. журнал' },
+ { value: 'acquaintance', label: 'Ознакомление' }
];
// Функция показа секции отчёта
diff --git a/public/style.css b/public/style.css
index 98b7480..6e9885b 100644
--- a/public/style.css
+++ b/public/style.css
@@ -5201,4 +5201,8 @@ button.btn-primary {
}
.btn-reset:hover {
background: #5a6268;
+}
+.task-type-badge.acquaintance {
+ background: #3498db; /* или любой подходящий цвет */
+ color: white;
}
\ No newline at end of file
diff --git a/public/tasks.js b/public/tasks.js
index 8e63fea..81e5011 100644
--- a/public/tasks.js
+++ b/public/tasks.js
@@ -628,6 +628,106 @@ function canUserAddFilesToTask(task) {
return false;
}
+// ==================== ФУНКЦИИ ДЛЯ ОЗНАКОМЛЕНИЯ ====================
+
+async function openAcquaintanceModal(taskId) {
+ try {
+ const response = await fetch(`/api/tasks/${taskId}`);
+ if (!response.ok) throw new Error('Ошибка загрузки задачи');
+ const task = await response.json();
+
+ // Заполняем модальное окно
+ document.getElementById('acquaintance-original-task-id').value = task.id;
+ document.getElementById('acquaintance-original-title').innerHTML = `
+
№${task.id} ${task.title}
+
Автор: ${task.creator_name}
+ `;
+
+ // Устанавливаем дату выполнения по умолчанию (завтра)
+ const tomorrow = new Date();
+ tomorrow.setDate(tomorrow.getDate() + 1);
+ document.getElementById('acquaintance-due-date').value = tomorrow.toISOString().split('T')[0];
+ document.getElementById('acquaintance-due-time').value = '19:00';
+
+ // Загружаем пользователей для выбора автора
+ await loadUsers(); // гарантируем, что users загружены
+ renderAcquaintanceAuthorsChecklist(users);
+
+ // Информация об исполнителе (текущий пользователь)
+ const executorInfo = document.getElementById('acquaintance-executor-info');
+ if (executorInfo) {
+ executorInfo.innerHTML = `Исполнитель: ${currentUser.name} (${currentUser.login})`;
+ }
+
+ // Отображаем модальное окно
+ document.getElementById('acquaintance-task-modal').style.display = 'block';
+ } catch (error) {
+ console.error('Ошибка открытия модального окна ознакомления:', error);
+ alert('Не удалось загрузить задачу');
+ }
+}
+
+function closeAcquaintanceModal() {
+ const modal = document.getElementById('acquaintance-task-modal');
+ if (modal) modal.style.display = 'none';
+
+ const authorSearch = document.getElementById('acquaintance-author-search');
+ if (authorSearch) authorSearch.value = '';
+
+ const userSearch = document.getElementById('acquaintance-user-search');
+ if (userSearch) userSearch.value = '';
+
+ acquaintanceSelectedUsers = [];
+ acquaintanceSelectedAuthor = null;
+}
+
+async function createAcquaintanceTask(event) {
+ event.preventDefault();
+
+ const originalTaskId = document.getElementById('acquaintance-original-task-id').value;
+ const dueDate = document.getElementById('acquaintance-due-date').value;
+ const dueTime = document.getElementById('acquaintance-due-time').value;
+ const fullDueDateTime = `${dueDate}T${dueTime}:00`;
+ const comment = document.getElementById('acquaintance-comment').value.trim();
+ const assignedUserIds = [currentUser.id]; // исполнитель – текущий пользователь
+ const creatorId = document.querySelector('input[name="acquaintance-author"]:checked')?.value;
+
+ if (!creatorId) {
+ alert('Выберите автора задачи');
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/tasks/acquaintance', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ originalTaskId,
+ dueDate: fullDueDateTime,
+ assignedUserIds,
+ creatorId,
+ comment
+ })
+ });
+
+ const result = await response.json();
+
+ if (response.ok) {
+ alert('Задача ознакомления успешно создана!');
+ closeAcquaintanceModal();
+ loadTasks();
+ loadActivityLogs();
+ } else {
+ alert(`Ошибка: ${result.error || 'Неизвестная ошибка'}`);
+ }
+ } catch (error) {
+ console.error('Ошибка создания задачи ознакомления:', error);
+ alert('Сетевая ошибка');
+ }
+}
+
// Добавляем отладочную функцию
function debugDocumentFields() {
console.log('=== ОТЛАДКА ПОЛЕЙ ДОКУМЕНТОВ ===');
@@ -647,4 +747,7 @@ function debugDocumentFields() {
window.debugDocumentFields = debugDocumentFields;
window.loadTasks = loadTasks;
window.updateAssignment = updateAssignment;
-window.renderTasksForActiveSection = renderTasksForActiveSection;
\ No newline at end of file
+window.renderTasksForActiveSection = renderTasksForActiveSection;
+window.openAcquaintanceModal = openAcquaintanceModal;
+window.closeAcquaintanceModal = closeAcquaintanceModal;
+window.createAcquaintanceTask = createAcquaintanceTask;
\ No newline at end of file
diff --git a/public/ui.js b/public/ui.js
index a6ec226..43e8bfa 100644
--- a/public/ui.js
+++ b/public/ui.js
@@ -1406,7 +1406,8 @@ function getTaskTypeName(type) {
'Social_educator': 'повод для обращения к cоциальному педагогу: ',
'hr': 'вопрос к кадровой службе',
'certificate': 'тип необходимой справки',
- 'e_journal': 'информацию для доступа к журналу'
+ 'e_journal': 'информацию для доступа к журналу',
+ 'acquaintance': 'Ознакомление с документом'
};
return typeNames[type] || 'задачу';
}
@@ -1422,7 +1423,8 @@ function getTaskTypeDisplayName(type) {
'Social_educator': 'Социальный педагог: ',
'hr': 'Кадры',
'certificate': 'Справка',
- 'e_journal': 'Эл. журнал'
+ 'e_journal': 'Эл. журнал',
+ 'acquaintance': 'Ознакомление'
};
return typeNames[type] || type;
}
@@ -1438,7 +1440,8 @@ function getTaskTypeIcon(type) {
'Social_educator': 'fas fa-brain',
'hr': 'fas fa-users',
'certificate': 'fas fa-file-certificate',
- 'e_journal': 'fas fa-book'
+ 'e_journal': 'fas fa-book',
+ 'acquaintance': 'fas fa-eye'
};
return icons[type] || 'fas fa-tasks';
}
diff --git a/public/users.js b/public/users.js
index 5db3e6d..d165e85 100644
--- a/public/users.js
+++ b/public/users.js
@@ -7,6 +7,8 @@ let filteredUsers = [];
let selectedUsers = [];
let editSelectedUsers = [];
let copySelectedUsers = [];
+let acquaintanceSelectedUsers = []; // исполнители для ознакомления
+let acquaintanceSelectedAuthor = null; // выбранный автор для ознакомления
// Переменные для пользовательских списков
let userLists = [];
@@ -662,6 +664,76 @@ async function saveListFromModal() {
closeListModal();
}
+// ==================== ФУНКЦИИ ДЛЯ ОЗНАКОМЛЕНИЯ (выбор исполнителей) ====================
+
+function renderAcquaintanceUsersChecklist(filtered = users) {
+ const container = document.getElementById('acquaintance-users-checklist');
+ if (!container) return;
+ container.innerHTML = filtered
+ .filter(user => user.id !== currentUser?.id)
+ .map(user => `
+
+
+
+ `).join('');
+}
+
+function toggleAcquaintanceUserSelection(checkbox, userId) {
+ if (checkbox.checked) {
+ if (!acquaintanceSelectedUsers.includes(userId)) acquaintanceSelectedUsers.push(userId);
+ } else {
+ acquaintanceSelectedUsers = acquaintanceSelectedUsers.filter(id => id !== userId);
+ }
+}
+
+async function filterAcquaintanceUsers() {
+ const search = document.getElementById('acquaintance-user-search')?.value.toLowerCase() || '';
+ let filtered = users.filter(user =>
+ user.name.toLowerCase().includes(search) ||
+ user.login.toLowerCase().includes(search) ||
+ (user.email && user.email.toLowerCase().includes(search))
+ );
+ renderAcquaintanceUsersChecklist(filtered);
+}
+
+// ==================== ФУНКЦИИ ДЛЯ ВЫБОРА АВТОРА В ЗАДАЧЕ ОЗНАКОМЛЕНИЯ ====================
+
+function renderAcquaintanceAuthorsChecklist(filtered = users) {
+ const container = document.getElementById('acquaintance-authors-checklist');
+ if (!container) return;
+ container.innerHTML = filtered
+ .filter(user => user.id !== currentUser?.id) // можно разрешить выбирать себя, если нужно
+ .map(user => `
+
+
+
+ `).join('');
+}
+
+function toggleAcquaintanceAuthorSelection(radio, userId) {
+ acquaintanceSelectedAuthor = radio.checked ? userId : null;
+}
+
+async function filterAcquaintanceAuthors() {
+ const search = document.getElementById('acquaintance-author-search')?.value.toLowerCase() || '';
+ let filtered = users.filter(user =>
+ user.name.toLowerCase().includes(search) ||
+ user.login.toLowerCase().includes(search) ||
+ (user.email && user.email.toLowerCase().includes(search))
+ );
+ renderAcquaintanceAuthorsChecklist(filtered);
+}
+
// ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================
function escapeHtml(text) {
@@ -705,6 +777,18 @@ window.closeListModal = closeListModal;
window.saveListFromModal = saveListFromModal;
window.filterListUsers = filterListUsers;
+// Экспорт функций для ознакомления (исполнители)
+window.renderAcquaintanceUsersChecklist = renderAcquaintanceUsersChecklist;
+window.toggleAcquaintanceUserSelection = toggleAcquaintanceUserSelection;
+window.filterAcquaintanceUsers = filterAcquaintanceUsers;
+window.acquaintanceSelectedUsers = acquaintanceSelectedUsers;
+
+// Экспорт функций для выбора автора
+window.renderAcquaintanceAuthorsChecklist = renderAcquaintanceAuthorsChecklist;
+window.toggleAcquaintanceAuthorSelection = toggleAcquaintanceAuthorSelection;
+window.filterAcquaintanceAuthors = filterAcquaintanceAuthors;
+window.acquaintanceSelectedAuthor = acquaintanceSelectedAuthor;
+
// Также экспортируем переменные, которые могут понадобиться в других скриптах
window.selectedUsers = selectedUsers;
window.editSelectedUsers = editSelectedUsers;
diff --git a/task-endpoints.js b/task-endpoints.js
index 5ae6f40..9a4dd96 100644
--- a/task-endpoints.js
+++ b/task-endpoints.js
@@ -2056,6 +2056,112 @@ app.post('/api/tasks/:taskId/copy', requireAuth, checkTaskCreationTimeout, (req,
);
});
});
+// API для создания задачи ознакомления с указанием автора
+app.post('/api/tasks/acquaintance', requireAuth, checkTaskCreationTimeout, (req, res) => {
+ const { originalTaskId, dueDate, assignedUserIds, creatorId, comment } = req.body;
+ const currentUserId = req.session.user.id;
+ const currentUser = req.session.user;
+
+ // Валидация
+ if (!originalTaskId) return res.status(400).json({ error: 'Не указана исходная задача' });
+ if (!dueDate) return res.status(400).json({ error: 'Дата выполнения обязательна' });
+ if (!assignedUserIds || assignedUserIds.length === 0) return res.status(400).json({ error: 'Не указаны исполнители' });
+ if (!creatorId) return res.status(400).json({ error: 'Не указан автор задачи' });
+
+ // Проверка прав: только admin и tasks могут создавать задачу от имени другого пользователя
+ if (currentUser.role !== 'admin' && currentUser.role !== 'tasks') {
+ return res.status(403).json({ error: 'Недостаточно прав для создания задачи от имени другого пользователя' });
+ }
+
+ // Исполнителем должен быть только текущий пользователь
+ if (assignedUserIds.length !== 1 || parseInt(assignedUserIds[0]) !== currentUserId) {
+ return res.status(400).json({ error: 'Исполнителем может быть только текущий пользователь' });
+ }
+
+ if (!req.taskCreationCheckPassed) {
+ return res.status(429).json({ error: 'Слишком частое создание задач' });
+ }
+
+ // Получаем исходную задачу с именем автора
+ db.get(`
+ SELECT t.*, u.name as creator_name
+ FROM tasks t
+ LEFT JOIN users u ON t.created_by = u.id
+ WHERE t.id = ?
+ `, [originalTaskId], (err, originalTask) => {
+ if (err || !originalTask) {
+ return res.status(404).json({ error: 'Исходная задача не найдена' });
+ }
+
+ const startDate = new Date().toISOString();
+ const taskType = 'acquaintance';
+
+ const newTitle = `Ознакомление: ${originalTask.title}`;
+ const newDescription = `Задача для ознакомления
+
+Оригинал: задача №${originalTask.id} "${originalTask.title}"
+Автор оригинала: ${originalTask.creator_name || 'Неизвестно'}
+${comment ? `Комментарий: ${comment}\n` : ''}
+---
+${originalTask.description || ''}`;
+
+ // Создаём задачу с указанным автором (creatorId)
+ db.run(
+ `INSERT INTO tasks
+ (title, description, created_by, original_task_id, start_date, due_date, task_type)
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
+ [newTitle, newDescription, creatorId, originalTaskId, startDate, dueDate, taskType],
+ function(err) {
+ if (err) {
+ console.error('❌ Ошибка создания задачи ознакомления:', err);
+ return res.status(500).json({ error: err.message });
+ }
+
+ const newTaskId = this.lastID;
+
+ // Обновляем таймаут создания
+ if (typeof updateLastTaskCreationTime === 'function') {
+ updateLastTaskCreationTime(currentUserId);
+ }
+
+ // Сохраняем метаданные
+ if (typeof saveTaskMetadata === 'function') {
+ saveTaskMetadata(newTaskId, newTitle, newDescription, creatorId, originalTaskId, startDate, dueDate);
+ }
+
+ // Назначаем исполнителя (текущий пользователь)
+ db.run(
+ `INSERT INTO task_assignments (task_id, user_id, start_date, due_date, status)
+ VALUES (?, ?, ?, ?, 'assigned')`,
+ [newTaskId, currentUserId, startDate, dueDate],
+ function(err) {
+ if (err) {
+ console.error('❌ Ошибка назначения исполнителя:', err);
+ db.run("DELETE FROM tasks WHERE id = ?", [newTaskId]);
+ return res.status(500).json({ error: err.message });
+ }
+
+ if (typeof logActivity === 'function') {
+ logActivity(newTaskId, currentUserId, 'TASK_CREATED',
+ `Создана задача ознакомления: ${newTitle} (автор: ${creatorId}, исполнитель: ${currentUserId})`);
+ }
+
+ if (typeof sendTaskNotifications === 'function') {
+ sendTaskNotifications('created', newTaskId, newTitle, newDescription, currentUserId);
+ }
+
+ res.json({
+ success: true,
+ taskId: newTaskId,
+ message: 'Задача ознакомления успешно создана',
+ timeoutInfo: { nextAllowedIn: 15 }
+ });
+ }
+ );
+ }
+ );
+ });
+});
}
module.exports = { setupTaskEndpoints, getApproverUsers };
\ No newline at end of file