копия задачи, доделать

This commit is contained in:
2026-02-25 23:17:24 +05:00
parent 0e838358f0
commit 908533929b
2 changed files with 532 additions and 63 deletions

View File

@@ -112,7 +112,7 @@
color: #7f8c8d;
}
.form-group input, .form-group select {
.form-group input, .form-group select, .form-group textarea {
padding: 10px 12px;
border: 1px solid #dce4ec;
border-radius: 5px;
@@ -120,7 +120,7 @@
transition: border-color 0.3s;
}
.form-group input:focus, .form-group select:focus {
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
outline: none;
border-color: #3498db;
}
@@ -273,7 +273,7 @@
.tasks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
@@ -427,6 +427,7 @@
align-items: center;
justify-content: center;
gap: 5px;
min-width: 80px;
}
.action-progress {
@@ -456,6 +457,15 @@
background: #2980b9;
}
.action-copy {
background: #9b59b6;
color: white;
}
.action-copy:hover {
background: #8e44ad;
}
.pagination {
display: flex;
justify-content: center;
@@ -501,6 +511,12 @@
color: #3498db;
}
.loading-small {
text-align: center;
padding: 10px;
color: #7f8c8d;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
@@ -534,9 +550,9 @@
background: white;
border-radius: 10px;
padding: 30px;
max-width: 500px;
max-width: 600px;
width: 90%;
max-height: 80vh;
max-height: 90vh;
overflow-y: auto;
}
@@ -618,7 +634,7 @@
cursor: pointer;
}
.upload-progress {
.upload-progress, .copy-progress {
margin-top: 15px;
padding: 10px;
background: #ecf0f1;
@@ -639,15 +655,42 @@
transition: width 0.3s;
}
.copy-status {
margin-top: 10px;
padding: 10px;
background: #ecf0f1;
border-radius: 5px;
font-size: 13px;
line-height: 1.8;
}
.alert {
padding: 15px;
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 5px;
margin-bottom: 20px;
display: none;
z-index: 2000;
max-width: 400px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.alert.show {
display: block;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.alert-success {
@@ -715,6 +758,12 @@
background: #3498db;
color: white;
}
small {
color: #95a5a6;
font-size: 12px;
margin-top: 3px;
}
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
@@ -874,10 +923,81 @@
</div>
</div>
<!-- Модальное окно копирования задачи -->
<div class="modal" id="copyModal">
<div class="modal-content">
<div class="modal-header">
<h3>Копирование задачи</h3>
<span class="modal-close" onclick="closeCopyModal()">&times;</span>
</div>
<div style="margin-bottom: 20px;">
<p>Вы копируете задачу: <strong id="copyTaskTitle"></strong></p>
<p style="font-size: 14px; color: #7f8c8d; margin-top: 5px;">
Автором новой задачи будете <strong id="currentUserName"></strong>
</p>
</div>
<div class="form-group" style="margin-bottom: 15px;">
<label>Целевой сервис <span style="color: #e74c3c;">*</span></label>
<select id="targetService" onchange="toggleTargetServiceInput()">
<option value="">-- Выберите сервис --</option>
<optgroup label="Сохраненные подключения" id="savedTargetConnections"></optgroup>
<option value="new">Указать новый сервис</option>
</select>
</div>
<div id="newServiceInputs" style="display: none;">
<div class="form-group" style="margin-bottom: 15px;">
<label>URL сервиса</label>
<input type="url" id="targetApiUrl" placeholder="https://example.com">
</div>
<div class="form-group" style="margin-bottom: 15px;">
<label>API ключ</label>
<input type="text" id="targetApiKey" placeholder="Введите API ключ">
</div>
</div>
<div class="form-group" style="margin-bottom: 15px;">
<label>Новые исполнители (ID пользователей через запятую)</label>
<input type="text" id="newAssignees" placeholder="123, 456, 789">
<small>Оставьте пустым, чтобы сохранить текущих исполнителей</small>
</div>
<div class="form-group" style="margin-bottom: 15px;">
<label>Новый срок выполнения (необязательно)</label>
<input type="datetime-local" id="newDueDate">
</div>
<div class="form-group" style="margin-bottom: 20px;">
<label style="display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="copyFiles" checked>
<span>Копировать файлы</span>
</label>
</div>
<div id="copyProgress" style="display: none;">
<div style="margin-bottom: 5px;">Копирование: <span id="copyPercent">0%</span></div>
<div class="progress-bar">
<div class="progress-fill" id="copyProgressBar" style="width: 0%;"></div>
</div>
<div id="copyStatus" class="copy-status"></div>
</div>
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button class="btn btn-secondary" onclick="closeCopyModal()">Отмена</button>
<button class="btn btn-success" onclick="copyTask()" id="copyBtn">
<i class="fas fa-copy"></i> Копировать задачу
</button>
</div>
</div>
</div>
<!-- Уведомления -->
<div id="alert" class="alert"></div>
<script>
// Глобальные переменные
let currentConnectionId = null;
let currentTasks = [];
let currentPage = 0;
@@ -885,6 +1005,8 @@
let pageSize = 50;
let currentTaskId = null;
let selectedFiles = [];
let copyTaskId = null;
let copyTaskTitle = '';
// Проверка авторизации
async function checkAuth() {
@@ -894,6 +1016,7 @@
if (data.user) {
document.getElementById('userName').textContent = data.user.name || data.user.login;
document.getElementById('currentUserName').textContent = data.user.name || data.user.login;
} else {
window.location.href = '/';
}
@@ -946,17 +1069,14 @@
function selectConnection(connectionId) {
currentConnectionId = connectionId;
// Подсвечиваем активное подключение
document.querySelectorAll('.connection-item').forEach(el => {
el.classList.remove('active');
});
event.currentTarget.classList.add('active');
// Активируем кнопки
document.getElementById('loadTasksBtn').disabled = false;
document.getElementById('refreshTasksBtn').disabled = false;
// Загружаем задачи
loadTasks();
}
@@ -1018,10 +1138,8 @@
if (data.success) {
showAlert('Подключение успешно установлено', 'success');
// Сохраняем ID подключения
currentConnectionId = data.connection.id;
// Показываем информацию о сервере
const serverInfo = document.getElementById('serverInfo');
serverInfo.innerHTML = `
<i class="fas fa-server"></i>
@@ -1031,18 +1149,14 @@
`;
serverInfo.style.display = 'block';
// Обновляем список сохраненных подключений
loadSavedConnections();
// Активируем кнопки
document.getElementById('loadTasksBtn').disabled = false;
document.getElementById('refreshTasksBtn').disabled = false;
// Очищаем поля ввода
document.getElementById('apiUrl').value = '';
document.getElementById('apiKey').value = '';
// Загружаем задачи
loadTasks();
} else {
showAlert(data.error || 'Ошибка подключения', 'danger');
@@ -1110,7 +1224,6 @@
const statusClass = `status-${task.assignment_status || 'default'}`;
const statusText = getStatusText(task.assignment_status);
// Форматируем даты
const createdDate = task.created_at ? new Date(task.created_at).toLocaleString() : 'Н/Д';
const dueDate = task.due_date ? new Date(task.due_date).toLocaleString() : 'Нет срока';
@@ -1154,6 +1267,9 @@
<button class="task-action-btn action-upload" onclick="openUploadModal('${task.id}')">
<i class="fas fa-upload"></i> Файлы
</button>
<button class="task-action-btn action-copy" onclick="openCopyModal('${task.id}', '${escapeHtml(task.title)}')">
<i class="fas fa-copy"></i> Копировать
</button>
</div>
</div>
`;
@@ -1176,11 +1292,11 @@
${files.map(file => `
<li class="file-item">
<i class="fas fa-file file-icon"></i>
<span class="file-name" title="${escapeHtml(file.filename)}">
${escapeHtml(file.filename)}
<span class="file-name" title="${escapeHtml(file.filename || file.original_name)}">
${escapeHtml(file.filename || file.original_name)}
</span>
<span class="file-size">${formatFileSize(file.file_size)}</span>
<a href="#" class="file-download" onclick="downloadFile('${taskId}', '${file.id}', '${escapeHtml(file.filename)}'); return false;">
<a href="#" class="file-download" onclick="downloadFile('${taskId}', '${file.id}', '${escapeHtml(file.filename || file.original_name)}'); return false;">
<i class="fas fa-download"></i>
</a>
</li>
@@ -1217,7 +1333,6 @@
if (data.success) {
showAlert(`Статус задачи изменен на "${getStatusText(status)}"`, 'success');
// Обновляем список задач
loadTasks();
} else {
showAlert(data.error || 'Ошибка обновления статуса', 'danger');
@@ -1253,7 +1368,7 @@
document.getElementById('progressPercent').textContent = '0%';
}
// Закрыть модальное окно
// Закрыть модальное окно загрузки
function closeUploadModal() {
document.getElementById('uploadModal').classList.remove('active');
currentTaskId = null;
@@ -1339,9 +1454,9 @@
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
if (data.success) {
showAlert(`Успешно загружено ${data.data?.files_uploaded || selectedFiles.length} файлов`, 'success');
showAlert(`Успешно загружено ${selectedFiles.length} файлов`, 'success');
closeUploadModal();
loadTasks(); // Обновляем список задач
loadTasks();
} else {
showAlert(data.error || 'Ошибка загрузки файлов', 'danger');
uploadBtn.disabled = false;
@@ -1394,6 +1509,173 @@
}
}
// Загрузка списка подключений для копирования
async function loadTargetConnections() {
try {
const response = await fetch('/api/client/connections/list');
const data = await response.json();
if (data.success) {
const select = document.getElementById('savedTargetConnections');
select.innerHTML = data.connections.map(conn =>
`<option value="${conn.id}">${conn.name} (${conn.url})</option>`
).join('');
}
} catch (error) {
console.error('Ошибка загрузки подключений:', error);
}
}
// Открыть модальное окно копирования
function openCopyModal(taskId, taskTitle) {
copyTaskId = taskId;
copyTaskTitle = taskTitle;
document.getElementById('copyTaskTitle').textContent = taskTitle;
loadTargetConnections();
document.getElementById('copyModal').classList.add('active');
}
// Закрыть модальное окно копирования
function closeCopyModal() {
document.getElementById('copyModal').classList.remove('active');
copyTaskId = null;
document.getElementById('targetService').value = '';
document.getElementById('newServiceInputs').style.display = 'none';
document.getElementById('targetApiUrl').value = '';
document.getElementById('targetApiKey').value = '';
document.getElementById('newAssignees').value = '';
document.getElementById('newDueDate').value = '';
document.getElementById('copyFiles').checked = true;
document.getElementById('copyProgress').style.display = 'none';
document.getElementById('copyBtn').disabled = false;
}
// Переключение между сохраненным и новым сервисом
function toggleTargetServiceInput() {
const targetService = document.getElementById('targetService').value;
document.getElementById('newServiceInputs').style.display =
targetService === 'new' ? 'block' : 'none';
}
// Копирование задачи
async function copyTask() {
if (!copyTaskId) return;
const targetService = document.getElementById('targetService').value;
const newAssignees = document.getElementById('newAssignees').value;
const newDueDate = document.getElementById('newDueDate').value;
const copyFiles = document.getElementById('copyFiles').checked;
if (!targetService) {
showAlert('Выберите целевой сервис', 'warning');
return;
}
let targetData = {};
if (targetService === 'new') {
const targetApiUrl = document.getElementById('targetApiUrl').value.trim();
const targetApiKey = document.getElementById('targetApiKey').value.trim();
if (!targetApiUrl || !targetApiKey) {
showAlert('Заполните URL и API ключ целевого сервиса', 'warning');
return;
}
targetData = {
target_api_url: targetApiUrl,
target_api_key: targetApiKey
};
} else {
targetData = {
target_connection_id: targetService
};
}
const requestData = {
...targetData,
copy_files: copyFiles
};
if (newAssignees) {
requestData.new_assignees = newAssignees.split(',').map(id => parseInt(id.trim()));
}
if (newDueDate) {
requestData.due_date = new Date(newDueDate).toISOString();
}
document.getElementById('copyProgress').style.display = 'block';
document.getElementById('copyBtn').disabled = true;
try {
const response = await fetch(`/api/client/tasks/${copyTaskId}/copy?connection_id=${currentConnectionId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
const data = await response.json();
if (data.success) {
let progress = 0;
const interval = setInterval(() => {
progress += 10;
document.getElementById('copyProgressBar').style.width = progress + '%';
document.getElementById('copyPercent').textContent = progress + '%';
if (progress >= 100) {
clearInterval(interval);
let statusText = `✅ Задача скопирована! Новый ID: ${data.data.new_task_id}`;
if (data.data.assignees && data.data.assignees !== 'не изменены') {
if (Array.isArray(data.data.assignees)) {
statusText += `<br>👥 Исполнители: ${data.data.assignees.join(', ')}`;
}
}
if (data.data.copied_files && data.data.copied_files.length > 0) {
const successCount = data.data.copied_files.filter(f => f.success).length;
const failCount = data.data.copied_files.filter(f => !f.success).length;
statusText += `<br>📁 Файлы: ${successCount} скопировано, ${failCount} ошибок`;
if (failCount > 0) {
const errors = data.data.copied_files
.filter(f => !f.success)
.map(f => f.original_name)
.join(', ');
statusText += `<br><small style="color: #e74c3c;">Ошибки: ${errors}</small>`;
}
}
document.getElementById('copyStatus').innerHTML = statusText;
setTimeout(() => {
closeCopyModal();
showAlert(`Задача скопирована в ${data.data.target_service}`, 'success');
}, 3000);
}
}, 200);
} else {
showAlert(data.error || 'Ошибка копирования задачи', 'danger');
document.getElementById('copyProgress').style.display = 'none';
document.getElementById('copyBtn').disabled = false;
}
} catch (error) {
console.error('Ошибка копирования:', error);
showAlert('Ошибка при копировании задачи', 'danger');
document.getElementById('copyProgress').style.display = 'none';
document.getElementById('copyBtn').disabled = false;
}
}
// Смена страницы
function changePage(delta) {
const newPage = currentPage + delta;
@@ -1449,6 +1731,7 @@
}
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")