Files
minicrm/public/loadMyCreatedTasks.js

747 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Скрипт для отображения задач, где пользователь является автором
// Глобальные переменные
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 = `
<span>🔄 Данные обновлены</span>
<span class="update-time">${new Date().toLocaleTimeString()}</span>
`;
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 = `
<div class="loading">
<div class="spinner"></div>
<p>Загрузка ваших задач...</p>
</div>
`;
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 = `
<div class="loading">Ошибка загрузки задач: ${error.message}</div>
`;
}
}
// Функция фильтрации задач
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 = '<div class="loading">У вас пока нет созданных задач</div>';
return;
}
if (myAuthorTasksFiltered.length === 0) {
container.innerHTML = '<div class="loading">Задачи не найдены</div>';
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 `
<div class="task-card" data-task-id="${task.id}">
<div class="task-header">
<div class="task-title" onclick="toggleMyTask(${task.id})" style="cursor: pointer; display: flex; justify-content: space-between; align-items: center;">
<div style="flex: 1;">
<span class="task-number">Задача №${task.id}</span>
<strong>${task.title || 'Без названия'}</strong>
${task.task_type ? `<span class="task-type-badge ${task.task_type}">${getTaskTypeDisplayName(task.task_type)}</span>` : ''}
${isClosed ? '<span class="closed-badge">Закрыта</span>' : ''}
${isCopy ? '<span class="copy-badge">Копия</span>' : ''}
${timeLeftInfo ? `<span class="deadline-badge ${timeLeftInfo.class}">${timeLeftInfo.text}</span>` : ''}
${task.assignments && task.assignments.length > 0 ?
`<span class="task-number">${task.assignments.map(a => a.user_name).join(', ')}</span>` : ''
}
</div>
<span class="task-status ${statusClass}">
Выполнить до: ${formatDateTime(task.due_date || task.created_at)}
</span>
<div class="expand-icon" style="margin-left: 10px; transition: transform 0.3s; transform: rotate(${isExpanded ? '180deg' : '0deg'});">
</div>
</div>
</div>
<div class="task-content ${isExpanded ? 'expanded' : ''}">
${isExpanded ? `
<div class="task-actions">
<button class="copy-btn" onclick="openTaskChat(${task.id})" title="Открыть чат">💬</button>
<button class="add-file-btn" onclick="openAddFileModal(${task.id})" title="Добавить файл">📎</button>
${currentUser && currentUser.login === 'minicrm' ?
`<button class="edit-btn" onclick="openEditModal(${task.id})" title="Редактировать">✏️</button>` : ''
}
${currentUser && currentUser.login === 'kalugin.o' ?
`<button class="manage-assignees-btn" onclick="openManageAssigneesModal(${task.id})" title="Управление исполнителями">👥</button>` : ''
}
${currentUser && (currentUser.role === 'tasks' || currentUser.role === 'admin') ?
`<button class="manage-assignees-btn" onclick="assignAdd_openModal(${task.id})" title="Управление исполнителями">🧑‍💼➕Добавить</button>` : ''
}
${currentUser && (currentUser.role === 'tasks' || currentUser.role === 'admin') ?
`<button class="manage-assignees-btn" onclick="assignRemove_openModal(${task.id})" title="Управление исполнителями">🧑‍💼❌Удалить</button>` : ''
}
<button class="copy-btn" onclick="openCopyModal(${task.id})" title="Создать копию">📋</button>
${currentUser && currentUser.login === 'minicrm' ?
`<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Вернуть на доработку">🔄</button>` : ''
}
${currentUser && currentUser.login === 'minicrm' ?
`<button class="close-btn" onclick="closeTask(${task.id})" title="Закрыть задачу">🔒</button>` : ''
}
<button class="delete-btn" onclick="deleteTask(${task.id})" title="Удалить">🗑️</button>
</div>
` : ''}
${isCopy && task.original_task_title ? `
<div class="task-original">
<small>Оригинал: "${task.original_task_title}" (создал: ${task.original_creator_name})</small>
</div>
` : ''}
<div class="task-description">${task.description || 'Нет описания'}</div>
${task.rework_comment ? `
<div class="rework-comment">
<strong>Комментарий к доработке:</strong> ${task.rework_comment}
</div>
` : ''}
<div class="file-list" id="files-${task.id}">
<strong>Файлы:</strong>
${task.files && task.files.length > 0 ?
renderGroupedFilesWithDelete ? renderGroupedFilesWithDelete(task) : renderGroupedFiles(task)
: '<span class="no-files">нет файлов</span>'
}
</div>
<div class="task-assignments">
<strong>Исполнители:</strong>
${task.assignments && task.assignments.length > 0 ?
renderAssignmentList(task.assignments, task.id, true) :
'<div>Не назначены</div>'
}
</div>
</div>
<div class="task-meta">
<small>
Создана: ${formatDateTime(task.start_date || task.created_at)}
| Выполнить до: ${formatDateTime(task.due_date || task.created_at)}
| Автор: ${task.creator_name}
| Тип: ${task.task_type ? `<span class="task-type-badge ${task.task_type}">${getTaskTypeDisplayName(task.task_type)}</span>` : ''}
</small>
${task.closed_at ? `<br><small>Закрыта: ${formatDateTime(task.closed_at)}</small>` : ''}
</div>
</div>
`;
}).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': 'Логопед',
'Social_educator': 'Социальный педагог',
'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 `
<div class="assignment ${isOverdue ? 'overdue' : ''} ${isRework ? 'rework' : ''}">
<span class="assignment-status ${statusClass}"></span>
<div style="flex: 1;">
<strong>${assignment.user_name}</strong>
${isCurrentUser ? '<small>(Вы)</small>' : ''}
${timeLeftInfo ? `<span class="deadline-indicator ${timeLeftInfo.class}">${timeLeftInfo.text}</span>` : ''}
${assignment.start_date || assignment.due_date ? `
<div class="assignment-dates">
${assignment.start_date ? `<small>Начало: ${formatDateTime(assignment.start_date)}</small>` : ''}
${assignment.due_date ? `<small>Выполнить до: ${formatDateTime(assignment.due_date)}</small>` : ''}
</div>
` : ''}
${assignment.rework_comment ? `
<div class="assignment-rework-comment">
<small><strong>Комментарий:</strong> ${assignment.rework_comment}</small>
</div>
` : ''}
</div>
<div class="action-buttons">
${isCurrentUser && assignment.status === 'assigned' ?
`<button onclick="updateStatus(${taskId}, ${assignment.user_id}, 'in_progress')">Приступить</button>` : ''}
${isCurrentUser && (assignment.status === 'in_progress' || assignment.status === 'overdue' || assignment.status === 'rework') ?
`<button onclick="updateStatus(${taskId}, ${assignment.user_id}, 'completed')">Выполнено</button>` : ''}
${isTaskCreator && assignment.status !== 'assigned' ?
`<button class="rework-btn" onclick="openReworkAssignmentModal(${taskId}, ${assignment.user_id}, '${assignment.user_name}')" title="Отправить на доработку">🔄Переделать</button>` : ''}
${isTaskCreator && assignment.status !== 'completed' ?
`<button class="force-complete-btn" onclick="forceCompleteAssignment(${taskId}, ${assignment.user_id}, '${assignment.user_name}')" title="Принудительно отметить как выполненное">✅ Завершить</button>` : ''}
${canEdit ?
`<button class="edit-date-btn" onclick="openEditAssignmentModal(${taskId}, ${assignment.user_id})" title="Редактировать сроки">📅</button>` : ''}
</div>
</div>
`;
}
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 = `
<div class="modal" id="add-file-modal">
<div class="modal-content">
<div class="modal-header">
<h3>Добавить файл к задаче: "${task.title}"</h3>
<span class="close" onclick="closeAddFileModal()">&times;</span>
</div>
<div class="modal-body">
<form id="add-file-form">
<input type="hidden" name="task_id" value="${taskId}">
<div class="form-group">
<label for="file-input">Выберите файл:</label>
<input type="file" id="file-input" name="files" multiple>
<small>Максимальный размер: 10MB</small>
</div>
<div class="form-group">
<label for="file-description">Описание файла (необязательно):</label>
<textarea id="file-description" name="description" rows="3"
placeholder="Описание файла..."></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn-cancel" onclick="closeAddFileModal()">Отмена</button>
<button type="submit" class="btn-primary">Добавить файл</button>
</div>
</form>
</div>
</div>
</div>
`;
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;