управления исполнителями

This commit is contained in:
2026-02-04 17:45:11 +05:00
parent 666be5a2bc
commit 689000798e
3 changed files with 995 additions and 0 deletions

View File

@@ -84,6 +84,7 @@ function renderTasks() {
${!isDeleted && !isClosed ? `
${canEdit ? `<button class="add-file-btn" onclick="openAddFileModal(${task.id})" title="Добавить файл">📎</button>` : ''}
${currentUser && currentUser.login === 'kalugin.o' ? `<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>` : ''}
<button class="copy-btn" onclick="openCopyModal(${task.id})" title="Создать копию">📋</button>
${canEdit ? `<button class="rework-btn" onclick="openReworkModal(${task.id})" title="Вернуть на доработку">🔄</button>` : ''}
${canEdit ? `<button class="close-btn" onclick="closeTask(${task.id})" title="Закрыть задачу">🔒</button>` : ''}
@@ -803,4 +804,437 @@ function getTaskTypeIcon(type) {
'e_journal': 'fas fa-book'
};
return icons[type] || 'fas fa-tasks';
}
// Функция для открытия модального окна управления исполнителями
async function openManageAssigneesModal(taskId) {
// Находим задачу
const task = tasks.find(t => t.id === taskId);
if (!task) {
alert('Задача не найдена');
return;
}
// Проверяем права
if (!canUserEditTask(task)) {
alert('У вас нет прав для управления исполнителями этой задачи');
return;
}
// Получаем текущих исполнителей
const currentAssignees = task.assignments || [];
// Получаем доступных для добавления исполнителей
try {
const response = await fetch(`/api/tasks/${taskId}/available-assignees`);
const availableUsers = await response.json();
// Получаем всех пользователей для выбора замены
const allUsersResponse = await fetch('/api/users');
const allUsers = await allUsersResponse.json();
// Создаем модальное окно
const modalHtml = `
<div class="modal" id="manage-assignees-modal">
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
<h3>Управление исполнителями задачи: "${task.title}"</h3>
<span class="close" onclick="closeManageAssigneesModal()">&times;</span>
</div>
<div class="modal-body">
<div class="tabs">
<button class="tab-btn active" onclick="switchManageTab('add')"> Добавить</button>
<button class="tab-btn" onclick="switchManageTab('remove')"> Удалить</button>
<button class="tab-btn" onclick="switchManageTab('replace-one')">🔄 Заменить одного</button>
<button class="tab-btn" onclick="switchManageTab('replace-all')">🔄 Заменить всех</button>
<button class="tab-btn" onclick="switchManageTab('assign-to-user')">👤 Назначить всем</button>
</div>
<div id="add-assignee-tab" class="tab-content active">
<h4>Добавить исполнителей</h4>
<p>Текущие исполнители: ${currentAssignees.map(a => a.user_name).join(', ') || 'нет'}</p>
<div class="user-search-box">
<input type="text" id="available-users-search" placeholder="Поиск пользователей..."
oninput="filterAvailableUsers()">
</div>
<div class="users-checklist" id="available-users-list" style="max-height: 200px; overflow-y: auto;">
${availableUsers.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" class="available-user-checkbox" value="${user.id}">
${user.name} (${user.email})
</label>
</div>
`).join('')}
</div>
<div class="modal-footer">
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
<button type="button" class="btn-primary" onclick="addAssignees(${taskId})">Добавить выбранных</button>
</div>
</div>
<div id="remove-assignee-tab" class="tab-content">
<h4>Удалить исполнителей</h4>
${currentAssignees.length > 0 ? `
<div class="users-checklist" style="max-height: 200px; overflow-y: auto;">
${currentAssignees.map(assignee => `
<div class="checkbox-item">
<label>
<input type="checkbox" class="remove-user-checkbox" value="${assignee.user_id}">
${assignee.user_name} (${assignee.user_login})
<small>Статус: ${getStatusText(assignee.status)}</small>
</label>
</div>
`).join('')}
</div>
<div class="modal-footer">
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
<button type="button" class="btn-danger" onclick="removeAssignees(${taskId})">Удалить выбранных</button>
</div>
` : '<p>Нет исполнителей для удаления</p>'}
</div>
<div id="replace-one-assignee-tab" class="tab-content">
<h4>Заменить одного исполнителя</h4>
${currentAssignees.length > 0 ? `
<div class="form-group">
<label>Выберите исполнителя для замены:</label>
<select id="old-assignee-select" class="form-control">
${currentAssignees.map(assignee => `
<option value="${assignee.user_id}">${assignee.user_name} (${assignee.user_login})</option>
`).join('')}
</select>
</div>
<div class="form-group">
<label>Выберите нового исполнителя:</label>
<select id="new-assignee-select" class="form-control">
<option value="">-- Выберите пользователя --</option>
${allUsers
.filter(user => !currentAssignees.some(a => a.user_id === user.id))
.map(user => `
<option value="${user.id}">${user.name} (${user.email})</option>
`).join('')}
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
<button type="button" class="btn-warning" onclick="replaceAssignee(${taskId})">Заменить</button>
</div>
` : '<p>Нет исполнителей для замены</p>'}
</div>
<div id="replace-all-assignee-tab" class="tab-content">
<h4>Заменить всех исполнителей</h4>
<p>Текущие исполнители будут удалены, будут назначены новые.</p>
<div class="user-search-box">
<input type="text" id="replace-all-users-search" placeholder="Поиск пользователей..."
oninput="filterReplaceAllUsers()">
</div>
<div class="users-checklist" id="replace-all-users-list" style="max-height: 200px; overflow-y: auto;">
${allUsers.map(user => `
<div class="checkbox-item">
<label>
<input type="checkbox" class="replace-all-user-checkbox" value="${user.id}">
${user.name} (${user.email})
</label>
</div>
`).join('')}
</div>
<div class="modal-footer">
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
<button type="button" class="btn-warning" onclick="replaceAllAssignees(${taskId})">Заменить всех</button>
</div>
</div>
<div id="assign-to-user-tab" class="tab-content">
<h4>Назначить всем выбранному пользователю</h4>
<p>Все текущие исполнители будут удалены, задача будет назначена только выбранному пользователю.</p>
<div class="form-group">
<label>Выберите пользователя:</label>
<select id="target-user-select" class="form-control">
<option value="">-- Выберите пользователя --</option>
${allUsers.map(user => `
<option value="${user.id}" ${user.login === 'kalugin.o' ? 'selected' : ''}>
${user.name} (${user.email})
</option>
`).join('')}
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn-cancel" onclick="closeManageAssigneesModal()">Отмена</button>
<button type="button" class="btn-primary" onclick="assignAllToUser(${taskId})">Назначить всем</button>
</div>
</div>
</div>
</div>
</div>
`;
// Добавляем модальное окно в DOM
const modalContainer = document.createElement('div');
modalContainer.innerHTML = modalHtml;
document.body.appendChild(modalContainer);
// Показываем модальное окно
setTimeout(() => {
document.getElementById('manage-assignees-modal').style.display = 'block';
}, 10);
} catch (error) {
console.error('Ошибка загрузки данных:', error);
alert('Ошибка загрузки данных пользователей');
}
}
// Функция для переключения вкладок в модальном окне
function switchManageTab(tabName) {
// Убираем активный класс со всех вкладок и контента
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
// Активируем выбранную вкладку
const activeTabBtn = document.querySelector(`.tab-btn[onclick*="${tabName}"]`);
if (activeTabBtn) activeTabBtn.classList.add('active');
const activeContent = document.getElementById(`${tabName}-tab`);
if (activeContent) activeContent.classList.add('active');
}
// Функция для фильтрации доступных пользователей
function filterAvailableUsers() {
const search = document.getElementById('available-users-search')?.value.toLowerCase() || '';
const checkboxes = document.querySelectorAll('.available-user-checkbox');
checkboxes.forEach(checkbox => {
const label = checkbox.parentElement.textContent.toLowerCase();
const isVisible = label.includes(search) || search === '';
checkbox.parentElement.parentElement.style.display = isVisible ? '' : 'none';
});
}
// Функция для фильтрации пользователей для замены всех
function filterReplaceAllUsers() {
const search = document.getElementById('replace-all-users-search')?.value.toLowerCase() || '';
const checkboxes = document.querySelectorAll('.replace-all-user-checkbox');
checkboxes.forEach(checkbox => {
const label = checkbox.parentElement.textContent.toLowerCase();
const isVisible = label.includes(search) || search === '';
checkbox.parentElement.parentElement.style.display = isVisible ? '' : 'none';
});
}
// Функция для добавления исполнителей
async function addAssignees(taskId) {
const selectedCheckboxes = document.querySelectorAll('.available-user-checkbox:checked');
const assigneeIds = Array.from(selectedCheckboxes).map(cb => cb.value);
if (assigneeIds.length === 0) {
alert('Выберите хотя бы одного пользователя для добавления');
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/assignees`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ assigneeIds })
});
if (response.ok) {
const result = await response.json();
alert(result.message);
closeManageAssigneesModal();
loadTasks(); // Перезагружаем задачи
} else {
const error = await response.json();
alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Сетевая ошибка при добавлении исполнителей');
}
}
// Функция для удаления исполнителей
async function removeAssignees(taskId) {
const selectedCheckboxes = document.querySelectorAll('.remove-user-checkbox:checked');
const assigneeIds = Array.from(selectedCheckboxes).map(cb => cb.value);
if (assigneeIds.length === 0) {
alert('Выберите хотя бы одного исполнителя для удаления');
return;
}
if (!confirm(`Вы уверены, что хотите удалить ${assigneeIds.length} исполнителей из задачи?`)) {
return;
}
// Удаляем каждого исполнителя по отдельности
const results = [];
for (const assigneeId of assigneeIds) {
try {
const response = await fetch(`/api/tasks/${taskId}/assignees/${assigneeId}`, {
method: 'DELETE'
});
if (response.ok) {
results.push({ id: assigneeId, success: true });
} else {
const error = await response.json();
results.push({ id: assigneeId, success: false, error: error.error });
}
} catch (error) {
results.push({ id: assigneeId, success: false, error: error.message });
}
}
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success);
if (failed.length > 0) {
alert(`Удалено ${successful} исполнителей. Ошибки: ${failed.map(f => `ID ${f.id}: ${f.error}`).join('; ')}`);
} else {
alert(`Успешно удалено ${successful} исполнителей`);
}
closeManageAssigneesModal();
loadTasks(); // Перезагружаем задачи
}
// Функция для замены одного исполнителя
async function replaceAssignee(taskId) {
const oldAssigneeId = document.getElementById('old-assignee-select').value;
const newAssigneeId = document.getElementById('new-assignee-select').value;
if (!oldAssigneeId || !newAssigneeId) {
alert('Выберите старого и нового исполнителя');
return;
}
if (!confirm('Вы уверены, что хотите заменить исполнителя?')) {
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/replace-assignee`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ oldAssigneeId, newAssigneeId })
});
if (response.ok) {
const result = await response.json();
alert(result.message);
closeManageAssigneesModal();
loadTasks(); // Перезагружаем задачи
} else {
const error = await response.json();
alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Сетевая ошибка при замене исполнителя');
}
}
// Функция для замены всех исполнителей
async function replaceAllAssignees(taskId) {
const selectedCheckboxes = document.querySelectorAll('.replace-all-user-checkbox:checked');
const newAssigneeIds = Array.from(selectedCheckboxes).map(cb => cb.value);
if (newAssigneeIds.length === 0) {
alert('Выберите хотя бы одного нового исполнителя');
return;
}
if (!confirm('Вы уверены, что хотите заменить всех исполнителей? Текущие исполнители будут удалены.')) {
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/replace-all-assignees`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ newAssigneeIds })
});
if (response.ok) {
const result = await response.json();
alert(result.message);
closeManageAssigneesModal();
loadTasks(); // Перезагружаем задачи
} else {
const error = await response.json();
alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Сетевая ошибка при замене исполнителей');
}
}
// Функция для назначения всех исполнителей одному пользователю
async function assignAllToUser(taskId) {
const targetUserId = document.getElementById('target-user-select').value;
if (!targetUserId) {
alert('Выберите пользователя для назначения');
return;
}
if (!confirm('Вы уверены, что хотите назначить задачу только этому пользователю? Все текущие исполнители будут удалены.')) {
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/assign-all-to-user`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ targetUserId })
});
if (response.ok) {
const result = await response.json();
alert(result.message);
closeManageAssigneesModal();
loadTasks(); // Перезагружаем задачи
} else {
const error = await response.json();
alert(`Ошибка: ${error.error || 'Неизвестная ошибка'}`);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Сетевая ошибка при назначении задачи');
}
}
// Функция для закрытия модального окна управления исполнителями
function closeManageAssigneesModal() {
const modal = document.getElementById('manage-assignees-modal');
if (modal) {
modal.style.display = 'none';
// Удаляем модальное окно из DOM через некоторое время
setTimeout(() => {
modal.parentElement.remove();
}, 300);
}
}