копия задачи, доделать
This commit is contained in:
264
api-client.js
264
api-client.js
@@ -4,6 +4,7 @@ const router = express.Router();
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const axios = require('axios');
|
||||
const FormData = require('form-data');
|
||||
|
||||
module.exports = function(app, db, upload) {
|
||||
|
||||
@@ -34,11 +35,6 @@ module.exports = function(app, db, upload) {
|
||||
|
||||
/**
|
||||
* POST /api/client/connect - Проверка подключения к внешнему сервису
|
||||
* Тело запроса:
|
||||
* {
|
||||
* "api_url": "https://example.com",
|
||||
* "api_key": "ваш_ключ"
|
||||
* }
|
||||
*/
|
||||
router.post('/api/client/connect', requireAuth, async (req, res) => {
|
||||
const { api_url, api_key } = req.body;
|
||||
@@ -51,7 +47,7 @@ module.exports = function(app, db, upload) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Нормализуем URL (убираем слеш в конце если есть)
|
||||
// Нормализуем URL
|
||||
const baseUrl = api_url.replace(/\/$/, '');
|
||||
|
||||
// Пробуем подключиться к сервису
|
||||
@@ -60,9 +56,9 @@ module.exports = function(app, db, upload) {
|
||||
'X-API-Key': api_key
|
||||
},
|
||||
params: {
|
||||
limit: 1 // Запрашиваем только одну задачу для проверки
|
||||
limit: 1
|
||||
},
|
||||
timeout: 10000 // 10 секунд таймаут
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
@@ -152,6 +148,25 @@ module.exports = function(app, db, upload) {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/client/connections/list - Получить список всех подключений для выбора
|
||||
*/
|
||||
router.get('/api/client/connections/list', requireAuth, (req, res) => {
|
||||
const connections = req.session.clientConnections || {};
|
||||
|
||||
const connectionsList = Object.values(connections).map(conn => ({
|
||||
id: conn.id,
|
||||
name: conn.name,
|
||||
url: conn.url,
|
||||
last_used: conn.last_used
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
connections: connectionsList
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/client/connections/:id - Удалить сохраненное подключение
|
||||
*/
|
||||
@@ -180,12 +195,6 @@ module.exports = function(app, db, upload) {
|
||||
|
||||
/**
|
||||
* GET /api/client/tasks - Получить список задач из внешнего сервиса
|
||||
* Параметры запроса:
|
||||
* - connection_id: ID сохраненного подключения (из сессии)
|
||||
* - api_url: URL сервиса (если нет connection_id)
|
||||
* - api_key: API ключ (если нет connection_id)
|
||||
* - status: фильтр по статусу (опционально)
|
||||
* - search: поиск по тексту (опционально)
|
||||
*/
|
||||
router.get('/api/client/tasks', requireAuth, async (req, res) => {
|
||||
const { connection_id, api_url, api_key, status, search, limit = 50, offset = 0 } = req.query;
|
||||
@@ -194,13 +203,11 @@ module.exports = function(app, db, upload) {
|
||||
let targetUrl = api_url;
|
||||
let targetKey = api_key;
|
||||
|
||||
// Если указан connection_id, берем данные из сессии
|
||||
if (connection_id && req.session.clientConnections && req.session.clientConnections[connection_id]) {
|
||||
const connection = req.session.clientConnections[connection_id];
|
||||
targetUrl = connection.url;
|
||||
targetKey = connection.api_key;
|
||||
|
||||
// Обновляем время последнего использования
|
||||
connection.last_used = new Date().toISOString();
|
||||
}
|
||||
|
||||
@@ -213,11 +220,9 @@ module.exports = function(app, db, upload) {
|
||||
try {
|
||||
const baseUrl = targetUrl.replace(/\/$/, '');
|
||||
|
||||
// Формируем параметры запроса
|
||||
const params = { limit, offset };
|
||||
if (status) params.status = status;
|
||||
|
||||
// Получаем список задач
|
||||
const response = await axios.get(`${baseUrl}/api/external/tasks`, {
|
||||
headers: {
|
||||
'X-API-Key': targetKey
|
||||
@@ -229,7 +234,6 @@ module.exports = function(app, db, upload) {
|
||||
if (response.data && response.data.success) {
|
||||
let tasks = response.data.tasks || [];
|
||||
|
||||
// Дополнительная фильтрация по поиску на стороне клиента
|
||||
if (search && tasks.length > 0) {
|
||||
const searchLower = search.toLowerCase();
|
||||
tasks = tasks.filter(task =>
|
||||
@@ -238,7 +242,6 @@ module.exports = function(app, db, upload) {
|
||||
);
|
||||
}
|
||||
|
||||
// Логируем действие
|
||||
const { logActivity } = require('./database');
|
||||
if (logActivity) {
|
||||
logActivity(0, userId, 'API_CLIENT_GET_TASKS',
|
||||
@@ -296,7 +299,6 @@ module.exports = function(app, db, upload) {
|
||||
router.get('/api/client/tasks/:taskId', requireAuth, async (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const { connection_id, api_url, api_key } = req.query;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
let targetUrl = api_url;
|
||||
let targetKey = api_key;
|
||||
@@ -357,11 +359,6 @@ module.exports = function(app, db, upload) {
|
||||
|
||||
/**
|
||||
* PUT /api/client/tasks/:taskId/status - Изменить статус задачи
|
||||
* Тело запроса:
|
||||
* {
|
||||
* "status": "in_progress" | "completed",
|
||||
* "comment": "Комментарий к изменению статуса" (опционально)
|
||||
* }
|
||||
*/
|
||||
router.put('/api/client/tasks/:taskId/status', requireAuth, async (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
@@ -404,7 +401,6 @@ module.exports = function(app, db, upload) {
|
||||
);
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
// Логируем действие
|
||||
const { logActivity } = require('./database');
|
||||
if (logActivity) {
|
||||
logActivity(0, userId, 'API_CLIENT_UPDATE_STATUS',
|
||||
@@ -448,6 +444,206 @@ module.exports = function(app, db, upload) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/client/tasks/:taskId/copy - Скопировать задачу в целевой сервис
|
||||
*/
|
||||
router.post('/api/client/tasks/:taskId/copy', requireAuth, async (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const {
|
||||
target_connection_id,
|
||||
target_api_url,
|
||||
target_api_key,
|
||||
new_assignees,
|
||||
due_date,
|
||||
copy_files = true
|
||||
} = req.body;
|
||||
const { connection_id } = req.query;
|
||||
const userId = req.session.user.id;
|
||||
|
||||
if (!connection_id && !req.session.clientConnections) {
|
||||
return res.status(400).json({
|
||||
error: 'Не указан источник (текущее подключение)'
|
||||
});
|
||||
}
|
||||
|
||||
let targetUrl = target_api_url;
|
||||
let targetKey = target_api_key;
|
||||
|
||||
if (target_connection_id && req.session.clientConnections && req.session.clientConnections[target_connection_id]) {
|
||||
const connection = req.session.clientConnections[target_connection_id];
|
||||
targetUrl = connection.url;
|
||||
targetKey = connection.api_key;
|
||||
}
|
||||
|
||||
if (!targetUrl || !targetKey) {
|
||||
return res.status(400).json({
|
||||
error: 'Не указан целевой сервис (URL или API ключ)'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = targetUrl.replace(/\/$/, '');
|
||||
const sourceConnection = req.session.clientConnections[connection_id];
|
||||
|
||||
if (!sourceConnection) {
|
||||
return res.status(400).json({ error: 'Исходное подключение не найдено' });
|
||||
}
|
||||
|
||||
// 1. Получаем исходную задачу
|
||||
const sourceResponse = await axios.get(
|
||||
`${sourceConnection.url}/api/external/tasks/${taskId}`,
|
||||
{
|
||||
headers: {
|
||||
'X-API-Key': sourceConnection.api_key
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
);
|
||||
|
||||
if (!sourceResponse.data || !sourceResponse.data.success) {
|
||||
return res.status(404).json({ error: 'Исходная задача не найдена' });
|
||||
}
|
||||
|
||||
const sourceTask = sourceResponse.data.task;
|
||||
|
||||
// 2. Создаем копию задачи в целевом сервисе
|
||||
const newTaskTitle = `Копия: ${sourceTask.title}`;
|
||||
|
||||
const taskData = {
|
||||
title: newTaskTitle,
|
||||
description: sourceTask.description || '',
|
||||
due_date: due_date || sourceTask.due_date,
|
||||
task_type: sourceTask.task_type || 'regular'
|
||||
};
|
||||
|
||||
if (new_assignees && new_assignees.length > 0) {
|
||||
taskData.assignedUsers = new_assignees;
|
||||
}
|
||||
|
||||
const createResponse = await axios.post(
|
||||
`${baseUrl}/api/external/tasks/create`,
|
||||
taskData,
|
||||
{
|
||||
headers: {
|
||||
'X-API-Key': targetKey,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 15000
|
||||
}
|
||||
);
|
||||
|
||||
if (!createResponse.data || !createResponse.data.success) {
|
||||
return res.status(500).json({
|
||||
error: 'Не удалось создать задачу в целевом сервисе'
|
||||
});
|
||||
}
|
||||
|
||||
const newTaskId = createResponse.data.taskId;
|
||||
|
||||
// 3. Копируем файлы, если нужно
|
||||
const copiedFiles = [];
|
||||
if (copy_files && sourceTask.files && sourceTask.files.length > 0) {
|
||||
for (const file of sourceTask.files) {
|
||||
try {
|
||||
const fileResponse = await axios({
|
||||
method: 'GET',
|
||||
url: `${sourceConnection.url}/api/external/tasks/${taskId}/files/${file.id}/download`,
|
||||
headers: {
|
||||
'X-API-Key': sourceConnection.api_key
|
||||
},
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('files', Buffer.from(fileResponse.data), {
|
||||
filename: file.filename || file.original_name || 'file',
|
||||
contentType: file.file_type || 'application/octet-stream'
|
||||
});
|
||||
|
||||
const uploadResponse = await axios.post(
|
||||
`${baseUrl}/api/external/tasks/${newTaskId}/files`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
...formData.getHeaders(),
|
||||
'X-API-Key': targetKey
|
||||
},
|
||||
timeout: 60000,
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity
|
||||
}
|
||||
);
|
||||
|
||||
copiedFiles.push({
|
||||
original_name: file.filename || file.original_name,
|
||||
success: uploadResponse.data && uploadResponse.data.success
|
||||
});
|
||||
} catch (fileError) {
|
||||
console.error(`❌ Ошибка копирования файла:`, fileError.message);
|
||||
copiedFiles.push({
|
||||
original_name: file.filename || file.original_name,
|
||||
success: false,
|
||||
error: fileError.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { logActivity } = require('./database');
|
||||
if (logActivity) {
|
||||
logActivity(0, userId, 'API_CLIENT_COPY_TASK',
|
||||
`Скопирована задача ${taskId} из ${sourceConnection.url} в ${baseUrl}. Новый ID: ${newTaskId}`);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Задача успешно скопирована${copiedFiles.length > 0 ? `, скопировано файлов: ${copiedFiles.filter(f => f.success).length}` : ''}`,
|
||||
data: {
|
||||
original_task_id: taskId,
|
||||
new_task_id: newTaskId,
|
||||
new_task_title: newTaskTitle,
|
||||
target_service: baseUrl,
|
||||
copied_files: copiedFiles,
|
||||
assignees: new_assignees || 'не изменены'
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка копирования задачи:', error.message);
|
||||
|
||||
let errorMessage = 'Ошибка копирования задачи';
|
||||
let statusCode = 500;
|
||||
|
||||
if (error.response) {
|
||||
if (error.response.status === 401) {
|
||||
errorMessage = 'Неверный API ключ для целевого сервиса';
|
||||
statusCode = 401;
|
||||
} else if (error.response.status === 403) {
|
||||
errorMessage = 'Нет прав для создания задачи в целевом сервисе';
|
||||
statusCode = 403;
|
||||
} else if (error.response.status === 404) {
|
||||
errorMessage = 'Исходная задача не найдена';
|
||||
statusCode = 404;
|
||||
} else {
|
||||
errorMessage = error.response.data?.error || `Ошибка сервера: ${error.response.status}`;
|
||||
statusCode = error.response.status;
|
||||
}
|
||||
} else if (error.code === 'ECONNREFUSED') {
|
||||
errorMessage = 'Целевой сервис недоступен';
|
||||
statusCode = 503;
|
||||
} else if (error.code === 'ETIMEDOUT') {
|
||||
errorMessage = 'Превышено время ожидания ответа';
|
||||
statusCode = 504;
|
||||
}
|
||||
|
||||
res.status(statusCode).json({
|
||||
error: errorMessage,
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/client/tasks/:taskId/files - Загрузить файлы в задачу
|
||||
*/
|
||||
@@ -470,7 +666,6 @@ module.exports = function(app, db, upload) {
|
||||
}
|
||||
|
||||
if (!targetUrl || !targetKey) {
|
||||
// Очищаем временные файлы
|
||||
req.files.forEach(file => {
|
||||
if (file.path && fs.existsSync(file.path)) {
|
||||
fs.unlinkSync(file.path);
|
||||
@@ -482,8 +677,6 @@ module.exports = function(app, db, upload) {
|
||||
try {
|
||||
const baseUrl = targetUrl.replace(/\/$/, '');
|
||||
|
||||
// Создаем FormData для отправки файлов
|
||||
const FormData = require('form-data');
|
||||
const formData = new FormData();
|
||||
|
||||
req.files.forEach(file => {
|
||||
@@ -493,7 +686,6 @@ module.exports = function(app, db, upload) {
|
||||
});
|
||||
});
|
||||
|
||||
// Отправляем файлы на внешний сервис
|
||||
const response = await axios.post(
|
||||
`${baseUrl}/api/external/tasks/${taskId}/files`,
|
||||
formData,
|
||||
@@ -502,13 +694,12 @@ module.exports = function(app, db, upload) {
|
||||
...formData.getHeaders(),
|
||||
'X-API-Key': targetKey
|
||||
},
|
||||
timeout: 60000, // 60 секунд для загрузки файлов
|
||||
timeout: 60000,
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity
|
||||
}
|
||||
);
|
||||
|
||||
// Удаляем временные файлы
|
||||
req.files.forEach(file => {
|
||||
if (file.path && fs.existsSync(file.path)) {
|
||||
fs.unlinkSync(file.path);
|
||||
@@ -516,7 +707,6 @@ module.exports = function(app, db, upload) {
|
||||
});
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
// Логируем действие
|
||||
const { logActivity } = require('./database');
|
||||
if (logActivity) {
|
||||
logActivity(0, userId, 'API_CLIENT_UPLOAD_FILES',
|
||||
@@ -532,7 +722,6 @@ module.exports = function(app, db, upload) {
|
||||
res.status(400).json({ error: 'Не удалось загрузить файлы' });
|
||||
}
|
||||
} catch (error) {
|
||||
// Удаляем временные файлы в случае ошибки
|
||||
req.files.forEach(file => {
|
||||
if (file.path && fs.existsSync(file.path)) {
|
||||
fs.unlinkSync(file.path);
|
||||
@@ -597,7 +786,6 @@ module.exports = function(app, db, upload) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Сначала получаем детали задачи, чтобы увидеть файлы
|
||||
const baseUrl = targetUrl.replace(/\/$/, '');
|
||||
|
||||
const response = await axios.get(`${baseUrl}/api/external/tasks/${taskId}`, {
|
||||
@@ -625,7 +813,7 @@ module.exports = function(app, db, upload) {
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/client/tasks/:taskId/files/:fileId/download - Скачать файл (через редирект)
|
||||
* GET /api/client/tasks/:taskId/files/:fileId/download - Скачать файл
|
||||
*/
|
||||
router.get('/api/client/tasks/:taskId/files/:fileId/download', requireAuth, async (req, res) => {
|
||||
const { taskId, fileId } = req.params;
|
||||
@@ -647,7 +835,6 @@ module.exports = function(app, db, upload) {
|
||||
try {
|
||||
const baseUrl = targetUrl.replace(/\/$/, '');
|
||||
|
||||
// Делаем запрос на скачивание файла
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: `${baseUrl}/api/external/tasks/${taskId}/files/${fileId}/download`,
|
||||
@@ -658,7 +845,6 @@ module.exports = function(app, db, upload) {
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
// Проксируем ответ клиенту
|
||||
const contentType = response.headers['content-type'] || 'application/octet-stream';
|
||||
const contentDisposition = response.headers['content-disposition'] || 'attachment';
|
||||
|
||||
|
||||
@@ -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