From 98e028fe191f5bc5d59968b8b32891ba5cefc08c Mon Sep 17 00:00:00 2001 From: kalugin66 Date: Fri, 13 Feb 2026 17:29:50 +0500 Subject: [PATCH] =?UTF-8?q?=D0=BC=D0=BE=D0=B8=20=D0=B7=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D1=87=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/index.html | 2 + public/loadMyCreatedTasks.js | 746 +++++++++++++++++++++++++++++++++++ public/navbar.js | 27 +- 3 files changed, 763 insertions(+), 12 deletions(-) create mode 100644 public/loadMyCreatedTasks.js diff --git a/public/index.html b/public/index.html index 623d91d..9571556 100644 --- a/public/index.html +++ b/public/index.html @@ -216,6 +216,7 @@
+

Задачи для исполнения

@@ -425,6 +426,7 @@ + diff --git a/public/loadMyCreatedTasks.js b/public/loadMyCreatedTasks.js new file mode 100644 index 0000000..f37a11f --- /dev/null +++ b/public/loadMyCreatedTasks.js @@ -0,0 +1,746 @@ +// Скрипт для отображения задач, где пользователь является автором + +// Глобальные переменные +let myAuthorTasks = []; +let myAuthorTasksFiltered = []; +let expandedMyTasks = new Set(); // Для отслеживания развернутых задач +let updateInterval = null; // Интервал обновления +let isUpdating = false; // Флаг для предотвращения множественных обновлений + +// Загрузка задач при открытии секции +function showMyTasksSection() { + showSection('mytasks'); + loadMyAuthorTasks(); + startAutoUpdate(); // Запускаем автообновление при открытии секции +} +// Остановка автообновления при уходе с секции +function hideMyTasksSection() { + stopAutoUpdate(); +} +// Запуск автоматического обновления +function startAutoUpdate() { + // Останавливаем предыдущий интервал, если был + stopAutoUpdate(); + + // Запускаем новый интервал (каждые 15 секунд) + updateInterval = setInterval(() => { + autoUpdateTasks(); + }, 15000); // 15000 мс = 15 секунд + + console.log('🔄 Автообновление задач запущено (каждые 15 сек)'); +} +// Остановка автоматического обновления +function stopAutoUpdate() { + if (updateInterval) { + clearInterval(updateInterval); + updateInterval = null; + console.log('⏹️ Автообновление задач остановлено'); + } +} +// Функция автоматического обновления +async function autoUpdateTasks() { + // Предотвращаем множественные обновления + if (isUpdating) { + console.log('⏳ Обновление уже выполняется, пропускаем...'); + return; + } + + // Проверяем, активна ли секция + const mytasksSection = document.getElementById('mytasks-section'); + if (!mytasksSection || !mytasksSection.classList.contains('active')) { + console.log('⏸️ Секция неактивна, автообновление приостановлено'); + stopAutoUpdate(); + return; + } + + isUpdating = true; + + try { + console.log('🔄 Автообновление задач...', new Date().toLocaleTimeString()); + + const response = await fetch('/api/kanban-tasks?days=62&filter=created'); + + if (!response.ok) { + throw new Error(`Ошибка сервера: ${response.status}`); + } + + const data = await response.json(); + const newTasks = data.tasks || []; + + // Проверяем, изменились ли данные + if (hasTasksChanged(myAuthorTasks, newTasks)) { + console.log('📊 Данные изменились, обновляем отображение'); + myAuthorTasks = newTasks; + filterMyTasks(); + + // Показываем уведомление об обновлении + showUpdateNotification(); + } else { + console.log('📊 Данные не изменились'); + } + + } catch (error) { + console.error('❌ Ошибка автообновления:', error); + } finally { + isUpdating = false; + } +} +// Показ уведомления об обновлении +function showUpdateNotification() { + const notification = document.createElement('div'); + notification.className = 'update-notification'; + notification.innerHTML = ` + 🔄 Данные обновлены + ${new Date().toLocaleTimeString()} + `; + + document.body.appendChild(notification); + + // Анимация появления и исчезновения + setTimeout(() => { + notification.classList.add('show'); + }, 10); + + setTimeout(() => { + notification.classList.remove('show'); + setTimeout(() => { + notification.remove(); + }, 300); + }, 2000); +} +// Проверка, изменились ли данные +function hasTasksChanged(oldTasks, newTasks) { + if (oldTasks.length !== newTasks.length) return true; + + // Создаем Map для быстрого сравнения + const oldMap = new Map(oldTasks.map(t => [t.id, t])); + + for (const newTask of newTasks) { + const oldTask = oldMap.get(newTask.id); + if (!oldTask) return true; + + // Проверяем важные поля + if (oldTask.kanbanStatus !== newTask.kanbanStatus) return true; + if (oldTask.title !== newTask.title) return true; + if (oldTask.description !== newTask.description) return true; + if (oldTask.due_date !== newTask.due_date) return true; + + // Проверяем изменения в назначениях + if (!areAssignmentsEqual(oldTask.assignments, newTask.assignments)) return true; + + // Проверяем изменения в файлах + if (!areFilesEqual(oldTask.files, newTask.files)) return true; + } + + return false; +} +// Проверка равенства назначений +function areAssignmentsEqual(oldAssignments, newAssignments) { + if (!oldAssignments && !newAssignments) return true; + if (!oldAssignments || !newAssignments) return false; + if (oldAssignments.length !== newAssignments.length) return false; + + const oldMap = new Map(oldAssignments.map(a => [a.user_id, a])); + + for (const newAss of newAssignments) { + const oldAss = oldMap.get(newAss.user_id); + if (!oldAss) return false; + if (oldAss.status !== newAss.status) return false; + } + + return true; +} +// Проверка равенства файлов +function areFilesEqual(oldFiles, newFiles) { + if (!oldFiles && !newFiles) return true; + if (!oldFiles || !newFiles) return false; + if (oldFiles.length !== newFiles.length) return false; + + const oldIds = new Set(oldFiles.map(f => f.id)); + const newIds = new Set(newFiles.map(f => f.id)); + + return oldIds.size === newIds.size && + [...oldIds].every(id => newIds.has(id)); +} + +// Основная функция загрузки задач +async function loadMyAuthorTasks() { + const container = document.getElementById('mytasks-list'); + + try { + container.innerHTML = ` +
+
+

Загрузка ваших задач...

+
+ `; + + const response = await fetch('/api/kanban-tasks?days=62&filter=created'); + + if (!response.ok) { + throw new Error(`Ошибка сервера: ${response.status}`); + } + + const data = await response.json(); + myAuthorTasks = data.tasks || []; + + filterMyTasks(); + + } catch (error) { + console.error('Ошибка загрузки задач:', error); + container.innerHTML = ` +
Ошибка загрузки задач: ${error.message}
+ `; + } +} + +// Функция фильтрации задач +function filterMyTasks() { + const statusFilter = document.getElementById('mytasks-status-filter')?.value || 'all'; + const searchText = document.getElementById('mytasks-search')?.value.toLowerCase() || ''; + + myAuthorTasksFiltered = myAuthorTasks.filter(task => { + if (statusFilter !== 'all') { + const taskStatus = task.kanbanStatus || 'assigned'; + if (taskStatus !== statusFilter) return false; + } + + if (searchText) { + const title = task.title || ''; + const description = task.description || ''; + const searchable = `${title} ${description}`.toLowerCase(); + if (!searchable.includes(searchText)) return false; + } + + return true; + }); + + renderMyAuthorTasks(); +} + +// Функция отображения задач в стиле ui.js +function renderMyAuthorTasks() { + const container = document.getElementById('mytasks-list'); + + if (!container) return; + + if (myAuthorTasks.length === 0) { + container.innerHTML = '
У вас пока нет созданных задач
'; + return; + } + + if (myAuthorTasksFiltered.length === 0) { + container.innerHTML = '
Задачи не найдены
'; + return; + } + + // Сортируем задачи по дате создания (новые сверху) + const sortedTasks = [...myAuthorTasksFiltered].sort((a, b) => + new Date(b.created_at || 0) - new Date(a.created_at || 0) + ); + + container.innerHTML = sortedTasks.map(task => { + const isExpanded = expandedMyTasks.has(task.id); + const overallStatus = task.kanbanStatus || 'assigned'; + const statusClass = getStatusClass(overallStatus); + const isClosed = task.closed_at !== null; + const isCopy = task.original_task_id !== null; + + const timeLeftInfo = getTimeLeftInfo(task); + + return ` +
+
+
+
+ Задача №${task.id} + ${task.title || 'Без названия'} + ${task.task_type ? `${getTaskTypeDisplayName(task.task_type)}` : ''} + ${isClosed ? 'Закрыта' : ''} + ${isCopy ? 'Копия' : ''} + ${timeLeftInfo ? `${timeLeftInfo.text}` : ''} + ${task.assignments && task.assignments.length > 0 ? + `${task.assignments.map(a => a.user_name).join(', ')}` : '' + } +
+ + Выполнить до: ${formatDateTime(task.due_date || task.created_at)} + +
+ ▼ +
+
+
+ +
+ ${isExpanded ? ` +
+ + + ${currentUser && currentUser.login === 'minicrm' ? + `` : '' + } + ${currentUser && currentUser.login === 'kalugin.o' ? + `` : '' + } + ${currentUser && (currentUser.role === 'tasks' || currentUser.role === 'admin') ? + `` : '' + } + ${currentUser && (currentUser.role === 'tasks' || currentUser.role === 'admin') ? + `` : '' + } + + ${currentUser && currentUser.login === 'minicrm' ? + `` : '' + } + ${currentUser && currentUser.login === 'minicrm' ? + `` : '' + } + +
+ ` : ''} + + ${isCopy && task.original_task_title ? ` +
+ Оригинал: "${task.original_task_title}" (создал: ${task.original_creator_name}) +
+ ` : ''} + +
${task.description || 'Нет описания'}
+ + ${task.rework_comment ? ` +
+ Комментарий к доработке: ${task.rework_comment} +
+ ` : ''} + +
+ Файлы: + ${task.files && task.files.length > 0 ? + renderGroupedFilesWithDelete ? renderGroupedFilesWithDelete(task) : renderGroupedFiles(task) + : 'нет файлов' + } +
+ +
+ Исполнители: + ${task.assignments && task.assignments.length > 0 ? + renderAssignmentList(task.assignments, task.id, true) : + '
Не назначены
' + } +
+
+ +
+ + Создана: ${formatDateTime(task.start_date || task.created_at)} + | Выполнить до: ${formatDateTime(task.due_date || task.created_at)} + | Автор: ${task.creator_name} + | Тип: ${task.task_type ? `${getTaskTypeDisplayName(task.task_type)}` : ''} + + ${task.closed_at ? `
Закрыта: ${formatDateTime(task.closed_at)}` : ''} +
+
+ `; + }).join(''); + + // Загружаем файлы для развернутых задач + expandedMyTasks.forEach(taskId => { + if (myAuthorTasks.some(t => t.id == taskId)) { + loadTaskFiles(taskId); + } + }); +} + +// Функция для переключения развернутого состояния задачи +function toggleMyTask(taskId) { + if (expandedMyTasks.has(taskId)) { + expandedMyTasks.delete(taskId); + } else { + expandedMyTasks.add(taskId); + loadTaskFiles(taskId); + } + renderMyAuthorTasks(); +} + + +function getStatusClass(status) { + switch (status) { + case 'deleted': return 'status-gray'; + case 'closed': return 'status-gray'; + case 'unassigned': return 'status-purple'; + case 'assigned': return 'status-red'; + case 'in_progress': return 'status-orange'; + case 'rework': return 'status-yellow'; + case 'overdue': return 'status-darkred'; + case 'completed': return 'status-green'; + default: return 'status-purple'; + } +} + +function getTaskTypeDisplayName(type) { + const typeNames = { + 'regular': 'Задача', + 'document': 'Документ', + 'it': 'ИТ', + 'ahch': 'АХЧ', + 'psychologist': 'Психолог', + 'speech_therapist': 'Логопед', + 'hr': 'Кадры', + 'certificate': 'Справка', + 'e_journal': 'Эл. журнал' + }; + return typeNames[type] || type; +} + +function formatDateTime(dateTimeString) { + if (!dateTimeString) return ''; + const date = new Date(dateTimeString); + return date.toLocaleString('ru-RU'); +} + +// Функция для рендеринга одного исполнителя (копия из ui.js с небольшими адаптациями) +function renderAssignment(assignment, taskId, canEdit) { + const statusClass = getStatusClass(assignment.status); + const isCurrentUser = assignment.user_id === currentUser.id; + const isOverdue = assignment.status === 'overdue'; + const isRework = assignment.status === 'rework'; + + const timeLeftInfo = getAssignmentTimeLeftInfo(assignment); + + const task = myAuthorTasks.find(t => t.id === taskId); + const isTaskCreator = task && parseInt(task.created_by) === currentUser.id; + + return ` +
+ +
+ ${assignment.user_name} + ${isCurrentUser ? '(Вы)' : ''} + ${timeLeftInfo ? `${timeLeftInfo.text}` : ''} + ${assignment.start_date || assignment.due_date ? ` +
+ ${assignment.start_date ? `Начало: ${formatDateTime(assignment.start_date)}` : ''} + ${assignment.due_date ? `Выполнить до: ${formatDateTime(assignment.due_date)}` : ''} +
+ ` : ''} + ${assignment.rework_comment ? ` +
+ Комментарий: ${assignment.rework_comment} +
+ ` : ''} +
+
+ ${isCurrentUser && assignment.status === 'assigned' ? + `` : ''} + ${isCurrentUser && (assignment.status === 'in_progress' || assignment.status === 'overdue' || assignment.status === 'rework') ? + `` : ''} + ${isTaskCreator && assignment.status !== 'assigned' ? + `` : ''} + ${isTaskCreator && assignment.status !== 'completed' ? + `` : ''} + ${canEdit ? + `` : ''} +
+
+ `; +} + +function getAssignmentTimeLeftInfo(assignment) { + if (!assignment.due_date || assignment.status === 'completed') return null; + + const dueDate = new Date(assignment.due_date); + const now = new Date(); + const timeLeft = dueDate.getTime() - now.getTime(); + const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000)); + + if (hoursLeft <= 0) return null; + + if (hoursLeft <= 24) { + return { + text: `Осталось ${hoursLeft}ч`, + class: 'deadline-24h' + }; + } else if (hoursLeft <= 48) { + return { + text: `Осталось ${hoursLeft}ч`, + class: 'deadline-48h' + }; + } + + return null; +} + +// Функция для фильтрации исполнителей +function filterAssignments(taskId) { + const filterInput = document.querySelector(`.assignment-filter-input[data-task-id="${taskId}"]`); + const scrollContainer = document.getElementById(`assignments-${taskId}`); + const filterCount = document.getElementById(`filter-count-${taskId}`); + + if (!filterInput || !scrollContainer) return; + + const searchTerm = filterInput.value.toLowerCase(); + const assignments = scrollContainer.querySelectorAll('.assignment'); + + let visibleCount = 0; + + assignments.forEach(assignment => { + const userName = assignment.querySelector('strong')?.textContent?.toLowerCase() || ''; + const userLogin = assignment.querySelector('small')?.textContent?.toLowerCase() || ''; + + const isVisible = userName.includes(searchTerm) || + userLogin.includes(searchTerm) || + searchTerm === ''; + + assignment.style.display = isVisible ? '' : 'none'; + + if (isVisible) { + visibleCount++; + } + }); + + if (filterCount) { + filterCount.textContent = `${visibleCount} из ${assignments.length} исполнителей`; + } +} + +// Функция для открытия модального окна добавления файла +function openAddFileModal(taskId) { + if (typeof window.openAddFileModal === 'function') { + return window.openAddFileModal(taskId); + } + + const task = myAuthorTasks.find(t => t.id === taskId); + if (!task) { + alert('Задача не найдена'); + return; + } + + const modalHtml = ` + + `; + + const modalContainer = document.createElement('div'); + modalContainer.innerHTML = modalHtml; + document.body.appendChild(modalContainer); + + document.getElementById('add-file-form').addEventListener('submit', async function(e) { + e.preventDefault(); + + const fileInput = document.getElementById('file-input'); + const description = document.getElementById('file-description').value; + + if (fileInput.files.length === 0) { + alert('Выберите файл для загрузки'); + return; + } + + const file = fileInput.files[0]; + const formData = new FormData(); + formData.append('files', file); + formData.append('task_id', taskId); + if (description) { + formData.append('description', description); + } + + try { + let response = await fetch(`/api/tasks/${taskId}/files`, { + method: 'POST', + body: formData + }); + + if (!response.ok) { + formData.delete('files'); + formData.append('file', file); + + response = await fetch(`/api/tasks/${taskId}/files`, { + method: 'POST', + body: formData + }); + } + + if (response.ok) { + alert('Файл успешно добавлен'); + await loadTaskFiles(taskId); + closeAddFileModal(); + + if (expandedMyTasks.has(taskId)) { + renderMyAuthorTasks(); + } + } else { + alert(`Ошибка при добавлении файла: ${response.status}`); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Сетевая ошибка при добавлении файла'); + } + }); + + setTimeout(() => { + document.getElementById('add-file-modal').style.display = 'block'; + }, 10); +} + +function closeAddFileModal() { + const modal = document.getElementById('add-file-modal'); + if (modal) { + modal.style.display = 'none'; + setTimeout(() => { + modal.parentElement.remove(); + }, 300); + } +} + +// Функция для открытия чата задачи +function openTaskChat(taskId) { + if (typeof window.openTaskChat === 'function') { + window.openTaskChat(taskId); + } else { + window.open(`/chat?task_id=${taskId}`, '_blank'); + } +} + +// Функция для открытия модального окна редактирования +function openEditModal(taskId) { + if (typeof window.openEditModal === 'function') { + window.openEditModal(taskId); + } else { + console.log('Открытие редактирования задачи:', taskId); + } +} + +// Функция для открытия модального окна копирования +function openCopyModal(taskId) { + if (typeof window.openCopyModal === 'function') { + window.openCopyModal(taskId); + } else { + console.log('Открытие копирования задачи:', taskId); + } +} + +// Функция для открытия модального окна доработки +function openReworkModal(taskId) { + if (typeof window.openReworkModal === 'function') { + window.openReworkModal(taskId); + } else { + console.log('Открытие доработки задачи:', taskId); + } +} + +// Функция для закрытия задачи +async function closeTask(taskId) { + if (!confirm('Вы уверены, что хотите закрыть задачу?')) return; + + try { + const response = await fetch(`/api/tasks/${taskId}/close`, { + method: 'PUT' + }); + + if (response.ok) { + alert('Задача закрыта'); + loadMyAuthorTasks(); + } else { + const error = await response.json(); + alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Сетевая ошибка при закрытии задачи'); + } +} + +// Функция для удаления задачи +async function deleteTask(taskId) { + if (!confirm('Вы уверены, что хотите удалить задачу?')) return; + + try { + const response = await fetch(`/api/tasks/${taskId}`, { + method: 'DELETE' + }); + + if (response.ok) { + alert('Задача удалена'); + loadMyAuthorTasks(); + } else { + const error = await response.json(); + alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Сетевая ошибка при удалении задачи'); + } +} + +// Функция для обновления статуса исполнителя +async function updateStatus(taskId, userId, newStatus) { + try { + const response = await fetch(`/api/tasks/${taskId}/assignments/${userId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ status: newStatus }) + }); + + if (response.ok) { + loadMyAuthorTasks(); + } else { + const error = await response.json(); + alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`); + } + } catch (error) { + console.error('Ошибка:', error); + alert('Сетевая ошибка при обновлении статуса'); + } +} + +// Автоматическая загрузка при открытии секции +document.addEventListener('DOMContentLoaded', () => { + const mytasksSection = document.getElementById('mytasks-section'); + if (mytasksSection && mytasksSection.style.display !== 'none') { + loadMyAuthorTasks(); + } +}); + +// Экспортируем функции в глобальную область +window.showMyTasksSection = showMyTasksSection; +window.loadMyAuthorTasks = loadMyAuthorTasks; +window.filterMyTasks = filterMyTasks; +window.toggleMyTask = toggleMyTask; +window.openAddFileModal = openAddFileModal; +window.closeAddFileModal = closeAddFileModal; +window.openTaskChat = openTaskChat; +window.openEditModal = openEditModal; +window.openCopyModal = openCopyModal; +window.openReworkModal = openReworkModal; +window.closeTask = closeTask; +window.deleteTask = deleteTask; +window.updateStatus = updateStatus; +window.filterAssignments = filterAssignments; +window.renderGroupedFiles = renderGroupedFiles; \ No newline at end of file diff --git a/public/navbar.js b/public/navbar.js index 9f84f26..47e7fef 100644 --- a/public/navbar.js +++ b/public/navbar.js @@ -20,20 +20,32 @@ function createNavigation() { text: "Главная", id: "home-btn" }, + + ]; +navButtons.push( { onclick: "showSection('tasks')", className: "nav-btn tasks", icon: "fas fa-list", - text: "Задачи", + text: "Все задачи", id: "tasks-btn" }, + { + onclick: "showSection('mytasks')", + className: "nav-btn my-tasks", + icon: "fas fa-user-edit", + text: "Мои задачи", + id: "my-tasks-btn" + }, { onclick: "showSection('create-task')", className: "nav-btn create", icon: "fas fa-plus-circle", text: "Создать задачу", id: "create-task-btn" - }, + } +); +navButtons.push( { onclick: "showKanbanSection()", className: "nav-btn kanban", @@ -48,17 +60,8 @@ function createNavigation() { text: "Личный кабинет", id: "profile-btn" } - ]; +); -if (currentUser && currentUser.role === 'admin') { - navButtons.push({ - onclick: "showSection('mytasks')", - className: "nav-btn my-tasks", - icon: "fas fa-user-edit", - text: "Мои задачи (Автор)", - id: "my-tasks-btn" - }); - } if (currentUser && currentUser.role === 'admin') { navButtons.push({ onclick: "showSection('runtasks')",