копия задачи, доделать
This commit is contained in:
@@ -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()">×</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, "&")
|
||||
.replace(/</g, "<")
|
||||
|
||||
Reference in New Issue
Block a user