1745 lines
63 KiB
HTML
1745 lines
63 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Клиент внешнего API - Управление задачами</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||
background: #f5f7fa;
|
||
color: #2c3e50;
|
||
line-height: 1.6;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 30px;
|
||
background: white;
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.header h1 i {
|
||
color: #3498db;
|
||
}
|
||
|
||
.user-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
}
|
||
|
||
.user-info span {
|
||
font-weight: 500;
|
||
color: #7f8c8d;
|
||
}
|
||
|
||
.logout-btn {
|
||
background: #e74c3c;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
transition: background 0.3s;
|
||
}
|
||
|
||
.logout-btn:hover {
|
||
background: #c0392b;
|
||
}
|
||
|
||
.connection-panel {
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
margin-bottom: 30px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.panel-title i {
|
||
color: #3498db;
|
||
}
|
||
|
||
.connection-form {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr auto;
|
||
gap: 15px;
|
||
align-items: end;
|
||
}
|
||
|
||
.form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
}
|
||
|
||
.form-group label {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #7f8c8d;
|
||
}
|
||
|
||
.form-group input, .form-group select, .form-group textarea {
|
||
padding: 10px 12px;
|
||
border: 1px solid #dce4ec;
|
||
border-radius: 5px;
|
||
font-size: 14px;
|
||
transition: border-color 0.3s;
|
||
}
|
||
|
||
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
|
||
outline: none;
|
||
border-color: #3498db;
|
||
}
|
||
|
||
.form-group input.error {
|
||
border-color: #e74c3c;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 5px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #3498db;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: #2980b9;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #95a5a6;
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #7f8c8d;
|
||
}
|
||
|
||
.btn-success {
|
||
background: #27ae60;
|
||
color: white;
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background: #229954;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #e74c3c;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: #c0392b;
|
||
}
|
||
|
||
.btn-warning {
|
||
background: #f39c12;
|
||
color: white;
|
||
}
|
||
|
||
.btn-warning:hover {
|
||
background: #e67e22;
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.saved-connections {
|
||
margin-top: 20px;
|
||
border-top: 1px solid #ecf0f1;
|
||
padding-top: 20px;
|
||
}
|
||
|
||
.connections-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.connection-item {
|
||
background: #ecf0f1;
|
||
padding: 10px 15px;
|
||
border-radius: 5px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
cursor: pointer;
|
||
transition: background 0.3s;
|
||
}
|
||
|
||
.connection-item:hover {
|
||
background: #d5dbdb;
|
||
}
|
||
|
||
.connection-item.active {
|
||
background: #3498db;
|
||
color: white;
|
||
}
|
||
|
||
.connection-item .remove-conn {
|
||
color: #e74c3c;
|
||
cursor: pointer;
|
||
font-size: 18px;
|
||
padding: 0 5px;
|
||
}
|
||
|
||
.connection-item .remove-conn:hover {
|
||
color: #c0392b;
|
||
}
|
||
|
||
.filter-panel {
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
margin-bottom: 30px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr 1fr auto;
|
||
gap: 15px;
|
||
align-items: end;
|
||
}
|
||
|
||
.tasks-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.tasks-header h2 {
|
||
font-size: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.tasks-header h2 i {
|
||
color: #3498db;
|
||
}
|
||
|
||
.tasks-count {
|
||
background: #3498db;
|
||
color: white;
|
||
padding: 5px 10px;
|
||
border-radius: 20px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.tasks-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.task-card {
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
transition: transform 0.3s, box-shadow 0.3s;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.task-card:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
.task-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: start;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.task-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #2c3e50;
|
||
flex: 1;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.task-status {
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.status-assigned { background: #3498db; color: white; }
|
||
.status-in_progress { background: #f39c12; color: white; }
|
||
.status-completed { background: #27ae60; color: white; }
|
||
.status-overdue { background: #e74c3c; color: white; }
|
||
.status-rework { background: #9b59b6; color: white; }
|
||
.status-default { background: #95a5a6; color: white; }
|
||
|
||
.task-description {
|
||
color: #7f8c8d;
|
||
font-size: 14px;
|
||
margin-bottom: 15px;
|
||
max-height: 100px;
|
||
overflow-y: auto;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.task-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-bottom: 15px;
|
||
font-size: 12px;
|
||
color: #95a5a6;
|
||
}
|
||
|
||
.task-meta-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.task-files {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.files-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.files-list {
|
||
list-style: none;
|
||
max-height: 150px;
|
||
overflow-y: auto;
|
||
border: 1px solid #ecf0f1;
|
||
border-radius: 5px;
|
||
padding: 5px;
|
||
}
|
||
|
||
.file-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 5px;
|
||
border-bottom: 1px solid #ecf0f1;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.file-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.file-icon {
|
||
color: #3498db;
|
||
}
|
||
|
||
.file-name {
|
||
flex: 1;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.file-size {
|
||
color: #95a5a6;
|
||
font-size: 10px;
|
||
}
|
||
|
||
.file-download {
|
||
color: #27ae60;
|
||
cursor: pointer;
|
||
text-decoration: none;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.file-download:hover {
|
||
color: #229954;
|
||
}
|
||
|
||
.task-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: auto;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.task-action-btn {
|
||
flex: 1;
|
||
padding: 8px;
|
||
border: none;
|
||
border-radius: 5px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: background 0.3s;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 5px;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.action-progress {
|
||
background: #f39c12;
|
||
color: white;
|
||
}
|
||
|
||
.action-progress:hover {
|
||
background: #e67e22;
|
||
}
|
||
|
||
.action-complete {
|
||
background: #27ae60;
|
||
color: white;
|
||
}
|
||
|
||
.action-complete:hover {
|
||
background: #229954;
|
||
}
|
||
|
||
.action-upload {
|
||
background: #3498db;
|
||
color: white;
|
||
}
|
||
|
||
.action-upload:hover {
|
||
background: #2980b9;
|
||
}
|
||
|
||
.action-sync {
|
||
background: #9b59b6;
|
||
color: white;
|
||
}
|
||
|
||
.action-sync:hover {
|
||
background: #8e44ad;
|
||
}
|
||
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.pagination-btn {
|
||
padding: 8px 12px;
|
||
border: 1px solid #dce4ec;
|
||
background: white;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.pagination-btn:hover:not(:disabled) {
|
||
background: #3498db;
|
||
color: white;
|
||
border-color: #3498db;
|
||
}
|
||
|
||
.pagination-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.pagination-info {
|
||
padding: 8px 12px;
|
||
background: #ecf0f1;
|
||
border-radius: 5px;
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
padding: 50px;
|
||
color: #7f8c8d;
|
||
}
|
||
|
||
.loading i {
|
||
animation: spin 1s linear infinite;
|
||
font-size: 30px;
|
||
color: #3498db;
|
||
}
|
||
|
||
.loading-small {
|
||
text-align: center;
|
||
padding: 10px;
|
||
color: #7f8c8d;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.no-tasks {
|
||
text-align: center;
|
||
padding: 50px;
|
||
color: #7f8c8d;
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0,0,0,0.5);
|
||
z-index: 1000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal.active {
|
||
display: flex;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 30px;
|
||
max-width: 600px;
|
||
width: 90%;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.modal-close {
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #7f8c8d;
|
||
}
|
||
|
||
.modal-close:hover {
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.upload-area {
|
||
border: 2px dashed #dce4ec;
|
||
border-radius: 5px;
|
||
padding: 30px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: border-color 0.3s;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.upload-area:hover {
|
||
border-color: #3498db;
|
||
}
|
||
|
||
.upload-area i {
|
||
font-size: 40px;
|
||
color: #3498db;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.upload-area p {
|
||
color: #7f8c8d;
|
||
}
|
||
|
||
.file-input {
|
||
display: none;
|
||
}
|
||
|
||
.selected-files {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.selected-file {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px;
|
||
background: #ecf0f1;
|
||
border-radius: 5px;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.selected-file .file-name {
|
||
flex: 1;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.selected-file .file-size {
|
||
color: #7f8c8d;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.selected-file .remove-file {
|
||
color: #e74c3c;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.upload-progress, .sync-progress {
|
||
margin-top: 15px;
|
||
padding: 10px;
|
||
background: #ecf0f1;
|
||
border-radius: 5px;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 10px;
|
||
background: #dce4ec;
|
||
border-radius: 5px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: #27ae60;
|
||
width: 0%;
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
.sync-status {
|
||
margin-top: 10px;
|
||
padding: 10px;
|
||
background: #ecf0f1;
|
||
border-radius: 5px;
|
||
font-size: 13px;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.alert {
|
||
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 {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
border: 1px solid #c3e6cb;
|
||
}
|
||
|
||
.alert-danger {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
border: 1px solid #f5c6cb;
|
||
}
|
||
|
||
.alert-warning {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
border: 1px solid #ffeeba;
|
||
}
|
||
|
||
.server-info {
|
||
margin-top: 15px;
|
||
padding: 10px;
|
||
background: #ecf0f1;
|
||
border-radius: 5px;
|
||
font-size: 13px;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.refresh-btn {
|
||
background: #3498db;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.refresh-btn:hover {
|
||
background: #2980b9;
|
||
}
|
||
|
||
.badge {
|
||
padding: 3px 8px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.badge-success {
|
||
background: #27ae60;
|
||
color: white;
|
||
}
|
||
|
||
.badge-warning {
|
||
background: #f39c12;
|
||
color: white;
|
||
}
|
||
|
||
.badge-info {
|
||
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>
|
||
<body>
|
||
<div class="container">
|
||
<!-- Шапка -->
|
||
<div class="header">
|
||
<h1>
|
||
<i class="fas fa-cloud-upload-alt"></i>
|
||
Клиент внешнего API
|
||
</h1>
|
||
<div class="user-info">
|
||
<span id="userName">Загрузка...</span>
|
||
<button class="logout-btn" onclick="logout()">
|
||
<i class="fas fa-sign-out-alt"></i> Выход
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Подключение к серверу -->
|
||
<div class="connection-panel">
|
||
<div class="panel-title">
|
||
<i class="fas fa-plug"></i>
|
||
Подключение к внешнему сервису https://minicrm.it25.su 940b4570dc4f43280b038b4417aac4cbb2133dbd98f22303d5c47a947f479a13
|
||
</div>
|
||
|
||
<div class="connection-form">
|
||
<div class="form-group">
|
||
<label>URL сервиса</label>
|
||
<input type="url" id="apiUrl" placeholder="https://example.com" value="">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>API ключ</label>
|
||
<input type="text" id="apiKey" placeholder="Введите API ключ">
|
||
</div>
|
||
<button class="btn btn-primary" onclick="connect()" id="connectBtn">
|
||
<i class="fas fa-link"></i> Подключиться
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Сохраненные подключения -->
|
||
<div class="saved-connections">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||
<span style="font-weight: 500;">Сохраненные подключения:</span>
|
||
<button class="btn btn-secondary" onclick="loadSavedConnections()" style="padding: 5px 10px;">
|
||
<i class="fas fa-sync-alt"></i> Обновить
|
||
</button>
|
||
</div>
|
||
<div class="connections-list" id="connectionsList">
|
||
<div class="loading-small">Загрузка...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Информация о сервере -->
|
||
<div id="serverInfo" class="server-info" style="display: none;"></div>
|
||
</div>
|
||
|
||
<!-- Фильтры -->
|
||
<div class="filter-panel">
|
||
<div class="form-group">
|
||
<label>Статус задач</label>
|
||
<select id="statusFilter">
|
||
<option value="">Все</option>
|
||
<option value="assigned">Назначена</option>
|
||
<option value="in_progress">В работе</option>
|
||
<option value="completed">Выполнена</option>
|
||
<option value="overdue">Просрочена</option>
|
||
<option value="rework">На доработке</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Поиск</label>
|
||
<input type="text" id="searchFilter" placeholder="Поиск по названию и описанию">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Лимит</label>
|
||
<select id="limitFilter">
|
||
<option value="20">20 задач</option>
|
||
<option value="50" selected>50 задач</option>
|
||
<option value="100">100 задач</option>
|
||
<option value="200">200 задач</option>
|
||
</select>
|
||
</div>
|
||
<button class="btn btn-primary" onclick="loadTasks()" id="loadTasksBtn" disabled>
|
||
<i class="fas fa-search"></i> Загрузить задачи
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Заголовок задач -->
|
||
<div class="tasks-header">
|
||
<h2>
|
||
<i class="fas fa-tasks"></i>
|
||
Задачи из внешнего сервиса
|
||
</h2>
|
||
<div style="display: flex; gap: 15px; align-items: center;">
|
||
<span class="tasks-count" id="tasksCount">0</span>
|
||
<button class="refresh-btn" onclick="loadTasks()" id="refreshTasksBtn" disabled>
|
||
<i class="fas fa-sync-alt"></i> Обновить
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Список задач -->
|
||
<div id="tasksContainer" class="tasks-grid">
|
||
<div class="loading">
|
||
<i class="fas fa-circle-notch"></i>
|
||
<p>Подключитесь к серверу для загрузки задач</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Пагинация -->
|
||
<div class="pagination" id="pagination" style="display: none;">
|
||
<button class="pagination-btn" id="prevPage" onclick="changePage(-1)" disabled>
|
||
<i class="fas fa-chevron-left"></i> Предыдущая
|
||
</button>
|
||
<span class="pagination-info" id="pageInfo">Страница 1 из 1</span>
|
||
<button class="pagination-btn" id="nextPage" onclick="changePage(1)" disabled>
|
||
Следующая <i class="fas fa-chevron-right"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно загрузки файлов -->
|
||
<div class="modal" id="uploadModal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3>Загрузка файлов</h3>
|
||
<span class="modal-close" onclick="closeUploadModal()">×</span>
|
||
</div>
|
||
|
||
<div class="upload-area" onclick="document.getElementById('fileInput').click()">
|
||
<i class="fas fa-cloud-upload-alt"></i>
|
||
<p>Нажмите для выбора файлов или перетащите их сюда</p>
|
||
<p style="font-size: 12px; color: #95a5a6;">Максимум 15 файлов</p>
|
||
</div>
|
||
|
||
<input type="file" id="fileInput" class="file-input" multiple onchange="handleFileSelect()">
|
||
|
||
<div class="selected-files" id="selectedFiles" style="display: none;">
|
||
<div style="font-weight: 500; margin-bottom: 10px;">Выбранные файлы:</div>
|
||
<div id="filesList"></div>
|
||
</div>
|
||
|
||
<div class="upload-progress" id="uploadProgress" style="display: none;">
|
||
<div style="margin-bottom: 5px;">Загрузка: <span id="progressPercent">0%</span></div>
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" id="progressBar" style="width: 0%;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||
<button class="btn btn-secondary" onclick="closeUploadModal()">Отмена</button>
|
||
<button class="btn btn-success" onclick="uploadFiles()" id="uploadBtn" disabled>
|
||
<i class="fas fa-upload"></i> Загрузить
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно синхронизации задачи -->
|
||
<div class="modal" id="syncModal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3>Синхронизация задачи</h3>
|
||
<span class="modal-close" onclick="closeSyncModal()">×</span>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 20px;">
|
||
<p>Вы синхронизируете задачу: <strong id="syncTaskTitle"></strong></p>
|
||
<p style="font-size: 14px; color: #7f8c8d; margin-top: 5px;">
|
||
<i class="fas fa-info-circle"></i> Исполнители будут сохранены как в исходной задаче
|
||
</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: 20px;">
|
||
<label style="display: flex; align-items: center; gap: 10px;">
|
||
<input type="checkbox" id="syncFiles" checked>
|
||
<span>Синхронизировать файлы</span>
|
||
</label>
|
||
<small style="display: block; margin-top: 5px; color: #7f8c8d;">
|
||
<i class="fas fa-exchange-alt"></i> При синхронизации задача будет обновлена в целевой системе,
|
||
если она там уже существует, или создана новая.
|
||
</small>
|
||
</div>
|
||
|
||
<div id="syncProgress" style="display: none;">
|
||
<div style="margin-bottom: 5px;">Синхронизация: <span id="syncPercent">0%</span></div>
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" id="syncProgressBar" style="width: 0%;"></div>
|
||
</div>
|
||
<div id="syncStatus" class="sync-status"></div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||
<button class="btn btn-secondary" onclick="closeSyncModal()">Отмена</button>
|
||
<button class="btn btn-success" onclick="syncTask()" id="syncBtn">
|
||
<i class="fas fa-sync-alt"></i> Синхронизировать задачу
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Уведомления -->
|
||
<div id="alert" class="alert"></div>
|
||
|
||
<script>
|
||
// Глобальные переменные
|
||
let currentConnectionId = null;
|
||
let currentTasks = [];
|
||
let currentPage = 0;
|
||
let totalTasks = 0;
|
||
let pageSize = 50;
|
||
let currentTaskId = null;
|
||
let selectedFiles = [];
|
||
let syncTaskId = null;
|
||
let syncTaskTitle = '';
|
||
|
||
// Проверка авторизации
|
||
async function checkAuth() {
|
||
try {
|
||
const response = await fetch('/api/user');
|
||
const data = await response.json();
|
||
|
||
if (data.user) {
|
||
document.getElementById('userName').textContent = data.user.name || data.user.login;
|
||
} else {
|
||
window.location.href = '/';
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка проверки авторизации:', error);
|
||
window.location.href = '/';
|
||
}
|
||
}
|
||
|
||
// Выход
|
||
async function logout() {
|
||
try {
|
||
await fetch('/api/logout', { method: 'POST' });
|
||
window.location.href = '/';
|
||
} catch (error) {
|
||
console.error('Ошибка при выходе:', error);
|
||
}
|
||
}
|
||
|
||
// Загрузка сохраненных подключений
|
||
async function loadSavedConnections() {
|
||
try {
|
||
const response = await fetch('/api/client/connections');
|
||
const data = await response.json();
|
||
|
||
const connectionsList = document.getElementById('connectionsList');
|
||
|
||
if (data.success && data.connections.length > 0) {
|
||
connectionsList.innerHTML = data.connections.map(conn => `
|
||
<div class="connection-item ${currentConnectionId === conn.id ? 'active' : ''}"
|
||
onclick="selectConnection('${conn.id}')">
|
||
<i class="fas fa-database"></i>
|
||
<span>${conn.name}</span>
|
||
<span class="remove-conn" onclick="event.stopPropagation(); removeConnection('${conn.id}')">
|
||
<i class="fas fa-times"></i>
|
||
</span>
|
||
</div>
|
||
`).join('');
|
||
} else {
|
||
connectionsList.innerHTML = '<div class="connection-item">Нет сохраненных подключений</div>';
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки подключений:', error);
|
||
document.getElementById('connectionsList').innerHTML =
|
||
'<div class="connection-item">Ошибка загрузки</div>';
|
||
}
|
||
}
|
||
|
||
// Выбор сохраненного подключения
|
||
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();
|
||
}
|
||
|
||
// Удаление подключения
|
||
async function removeConnection(connectionId) {
|
||
if (!confirm('Удалить это подключение?')) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/client/connections/${connectionId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
if (currentConnectionId === connectionId) {
|
||
currentConnectionId = null;
|
||
document.getElementById('loadTasksBtn').disabled = true;
|
||
document.getElementById('refreshTasksBtn').disabled = true;
|
||
document.getElementById('tasksContainer').innerHTML =
|
||
'<div class="loading"><i class="fas fa-plug"></i><p>Подключитесь к серверу</p></div>';
|
||
document.getElementById('tasksCount').textContent = '0';
|
||
document.getElementById('serverInfo').style.display = 'none';
|
||
}
|
||
loadSavedConnections();
|
||
showAlert('Подключение удалено', 'success');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка удаления подключения:', error);
|
||
showAlert('Ошибка при удалении подключения', 'danger');
|
||
}
|
||
}
|
||
|
||
// Подключение к серверу
|
||
async function connect() {
|
||
const apiUrl = document.getElementById('apiUrl').value.trim();
|
||
const apiKey = document.getElementById('apiKey').value.trim();
|
||
|
||
if (!apiUrl || !apiKey) {
|
||
showAlert('Заполните URL сервиса и API ключ', 'warning');
|
||
return;
|
||
}
|
||
|
||
const connectBtn = document.getElementById('connectBtn');
|
||
connectBtn.disabled = true;
|
||
connectBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Подключение...';
|
||
|
||
try {
|
||
const response = await fetch('/api/client/connect', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ api_url: apiUrl, api_key: apiKey })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showAlert('Подключение успешно установлено', 'success');
|
||
|
||
currentConnectionId = data.connection.id;
|
||
|
||
const serverInfo = document.getElementById('serverInfo');
|
||
serverInfo.innerHTML = `
|
||
<i class="fas fa-server"></i>
|
||
<strong>Сервер:</strong> ${data.connection.url}<br>
|
||
<strong>Пользователь:</strong> ${data.server_info.user}<br>
|
||
<strong>Задач на сервере:</strong> ${data.server_info.tasks_count}
|
||
`;
|
||
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');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка подключения:', error);
|
||
showAlert('Ошибка при подключении к серверу', 'danger');
|
||
} finally {
|
||
connectBtn.disabled = false;
|
||
connectBtn.innerHTML = '<i class="fas fa-link"></i> Подключиться';
|
||
}
|
||
}
|
||
|
||
// Загрузка задач
|
||
async function loadTasks() {
|
||
if (!currentConnectionId) {
|
||
showAlert('Сначала выберите подключение', 'warning');
|
||
return;
|
||
}
|
||
|
||
const statusFilter = document.getElementById('statusFilter').value;
|
||
const searchFilter = document.getElementById('searchFilter').value;
|
||
const limit = parseInt(document.getElementById('limitFilter').value);
|
||
|
||
pageSize = limit;
|
||
|
||
const tasksContainer = document.getElementById('tasksContainer');
|
||
tasksContainer.innerHTML = '<div class="loading"><i class="fas fa-circle-notch fa-spin"></i><p>Загрузка задач...</p></div>';
|
||
|
||
try {
|
||
let url = `/api/client/tasks?connection_id=${currentConnectionId}&limit=${limit}&offset=${currentPage * limit}`;
|
||
if (statusFilter) url += `&status=${statusFilter}`;
|
||
if (searchFilter) url += `&search=${encodeURIComponent(searchFilter)}`;
|
||
|
||
const response = await fetch(url);
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
currentTasks = data.tasks || [];
|
||
totalTasks = data.meta.total;
|
||
|
||
document.getElementById('tasksCount').textContent = totalTasks;
|
||
|
||
if (currentTasks.length === 0) {
|
||
tasksContainer.innerHTML = '<div class="no-tasks"><i class="fas fa-folder-open"></i><p>Задачи не найдены</p></div>';
|
||
} else {
|
||
renderTasks(currentTasks);
|
||
}
|
||
|
||
updatePagination();
|
||
} else {
|
||
tasksContainer.innerHTML = `<div class="no-tasks"><i class="fas fa-exclamation-triangle"></i><p>Ошибка: ${data.error || 'Неизвестная ошибка'}</p></div>`;
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки задач:', error);
|
||
tasksContainer.innerHTML = '<div class="no-tasks"><i class="fas fa-exclamation-triangle"></i><p>Ошибка загрузки задач</p></div>';
|
||
}
|
||
}
|
||
|
||
// Отрисовка задач
|
||
function renderTasks(tasks) {
|
||
const container = document.getElementById('tasksContainer');
|
||
|
||
container.innerHTML = tasks.map(task => {
|
||
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() : 'Нет срока';
|
||
|
||
return `
|
||
<div class="task-card" id="task-${task.id}">
|
||
<div class="task-header">
|
||
<div class="task-title">${escapeHtml(task.title || 'Без названия')}</div>
|
||
<div class="task-status ${statusClass}">${statusText}</div>
|
||
</div>
|
||
|
||
<div class="task-description">
|
||
${escapeHtml(task.description || 'Нет описания')}
|
||
</div>
|
||
|
||
<div class="task-meta">
|
||
<div class="task-meta-item">
|
||
<i class="fas fa-calendar-alt"></i>
|
||
<span>Создана: ${createdDate}</span>
|
||
</div>
|
||
<div class="task-meta-item">
|
||
<i class="fas fa-clock"></i>
|
||
<span>Срок: ${dueDate}</span>
|
||
</div>
|
||
<div class="task-meta-item">
|
||
<i class="fas fa-user"></i>
|
||
<span>Автор: ${escapeHtml(task.creator_name || 'Н/Д')}</span>
|
||
</div>
|
||
</div>
|
||
|
||
${renderFiles(task.files, task.id)}
|
||
|
||
<div class="task-actions">
|
||
${task.assignment_status !== 'completed' ? `
|
||
<button class="task-action-btn action-progress" onclick="updateTaskStatus('${task.id}', 'in_progress')">
|
||
<i class="fas fa-play"></i> В работу
|
||
</button>
|
||
<button class="task-action-btn action-complete" onclick="updateTaskStatus('${task.id}', 'completed')">
|
||
<i class="fas fa-check"></i> Завершить
|
||
</button>
|
||
` : ''}
|
||
<button class="task-action-btn action-upload" onclick="openUploadModal('${task.id}')">
|
||
<i class="fas fa-upload"></i> Файлы
|
||
</button>
|
||
<button class="task-action-btn action-sync" onclick="openSyncModal('${task.id}', '${escapeHtml(task.title)}')">
|
||
<i class="fas fa-sync-alt"></i> Синхронизировать
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Отрисовка файлов задачи
|
||
function renderFiles(files, taskId) {
|
||
if (!files || files.length === 0) {
|
||
return '';
|
||
}
|
||
|
||
return `
|
||
<div class="task-files">
|
||
<div class="files-title">
|
||
<i class="fas fa-paperclip"></i>
|
||
Файлы (${files.length})
|
||
</div>
|
||
<ul class="files-list">
|
||
${files.map(file => `
|
||
<li class="file-item">
|
||
<i class="fas fa-file file-icon"></i>
|
||
<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 || file.original_name)}'); return false;">
|
||
<i class="fas fa-download"></i>
|
||
</a>
|
||
</li>
|
||
`).join('')}
|
||
</ul>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Обновление статуса задачи
|
||
async function updateTaskStatus(taskId, status) {
|
||
if (!currentConnectionId) return;
|
||
|
||
const comment = status === 'completed' ?
|
||
prompt('Введите комментарий к завершению (необязательно):') :
|
||
null;
|
||
|
||
const button = document.querySelector(`#task-${taskId} .action-${status === 'in_progress' ? 'progress' : 'complete'}`);
|
||
if (button) {
|
||
button.disabled = true;
|
||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Обновление...';
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/client/tasks/${taskId}/status?connection_id=${currentConnectionId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ status, comment })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
showAlert(`Статус задачи изменен на "${getStatusText(status)}"`, 'success');
|
||
loadTasks();
|
||
} else {
|
||
showAlert(data.error || 'Ошибка обновления статуса', 'danger');
|
||
if (button) {
|
||
button.disabled = false;
|
||
button.innerHTML = status === 'in_progress' ?
|
||
'<i class="fas fa-play"></i> В работу' :
|
||
'<i class="fas fa-check"></i> Завершить';
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка обновления статуса:', error);
|
||
showAlert('Ошибка при обновлении статуса', 'danger');
|
||
if (button) {
|
||
button.disabled = false;
|
||
button.innerHTML = status === 'in_progress' ?
|
||
'<i class="fas fa-play"></i> В работу' :
|
||
'<i class="fas fa-check"></i> Завершить';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Открыть модальное окно загрузки файлов
|
||
function openUploadModal(taskId) {
|
||
currentTaskId = taskId;
|
||
document.getElementById('uploadModal').classList.add('active');
|
||
selectedFiles = [];
|
||
document.getElementById('selectedFiles').style.display = 'none';
|
||
document.getElementById('filesList').innerHTML = '';
|
||
document.getElementById('uploadBtn').disabled = true;
|
||
document.getElementById('uploadProgress').style.display = 'none';
|
||
document.getElementById('progressBar').style.width = '0%';
|
||
document.getElementById('progressPercent').textContent = '0%';
|
||
}
|
||
|
||
// Закрыть модальное окно загрузки
|
||
function closeUploadModal() {
|
||
document.getElementById('uploadModal').classList.remove('active');
|
||
currentTaskId = null;
|
||
}
|
||
|
||
// Обработка выбора файлов
|
||
function handleFileSelect() {
|
||
const files = document.getElementById('fileInput').files;
|
||
selectedFiles = Array.from(files);
|
||
|
||
if (selectedFiles.length > 0) {
|
||
const filesList = document.getElementById('filesList');
|
||
filesList.innerHTML = selectedFiles.map((file, index) => `
|
||
<div class="selected-file">
|
||
<i class="fas fa-file"></i>
|
||
<span class="file-name">${escapeHtml(file.name)}</span>
|
||
<span class="file-size">${formatFileSize(file.size)}</span>
|
||
<span class="remove-file" onclick="removeFile(${index})">
|
||
<i class="fas fa-times"></i>
|
||
</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
document.getElementById('selectedFiles').style.display = 'block';
|
||
document.getElementById('uploadBtn').disabled = false;
|
||
} else {
|
||
document.getElementById('selectedFiles').style.display = 'none';
|
||
document.getElementById('uploadBtn').disabled = true;
|
||
}
|
||
}
|
||
|
||
// Удалить файл из списка
|
||
function removeFile(index) {
|
||
selectedFiles.splice(index, 1);
|
||
|
||
if (selectedFiles.length > 0) {
|
||
const filesList = document.getElementById('filesList');
|
||
filesList.innerHTML = selectedFiles.map((file, i) => `
|
||
<div class="selected-file">
|
||
<i class="fas fa-file"></i>
|
||
<span class="file-name">${escapeHtml(file.name)}</span>
|
||
<span class="file-size">${formatFileSize(file.size)}</span>
|
||
<span class="remove-file" onclick="removeFile(${i})">
|
||
<i class="fas fa-times"></i>
|
||
</span>
|
||
</div>
|
||
`).join('');
|
||
} else {
|
||
document.getElementById('selectedFiles').style.display = 'none';
|
||
document.getElementById('uploadBtn').disabled = true;
|
||
}
|
||
}
|
||
|
||
// Загрузка файлов
|
||
async function uploadFiles() {
|
||
if (!currentConnectionId || !currentTaskId || selectedFiles.length === 0) return;
|
||
|
||
const formData = new FormData();
|
||
selectedFiles.forEach(file => {
|
||
formData.append('files', file);
|
||
});
|
||
|
||
const uploadBtn = document.getElementById('uploadBtn');
|
||
const progressDiv = document.getElementById('uploadProgress');
|
||
const progressBar = document.getElementById('progressBar');
|
||
const progressPercent = document.getElementById('progressPercent');
|
||
|
||
uploadBtn.disabled = true;
|
||
progressDiv.style.display = 'block';
|
||
|
||
try {
|
||
const xhr = new XMLHttpRequest();
|
||
|
||
xhr.upload.addEventListener('progress', (e) => {
|
||
if (e.lengthComputable) {
|
||
const percent = Math.round((e.loaded / e.total) * 100);
|
||
progressBar.style.width = percent + '%';
|
||
progressPercent.textContent = percent + '%';
|
||
}
|
||
});
|
||
|
||
xhr.addEventListener('load', () => {
|
||
if (xhr.status === 200) {
|
||
const data = JSON.parse(xhr.responseText);
|
||
if (data.success) {
|
||
showAlert(`Успешно загружено ${selectedFiles.length} файлов`, 'success');
|
||
closeUploadModal();
|
||
loadTasks();
|
||
} else {
|
||
showAlert(data.error || 'Ошибка загрузки файлов', 'danger');
|
||
uploadBtn.disabled = false;
|
||
}
|
||
} else {
|
||
showAlert('Ошибка загрузки файлов', 'danger');
|
||
uploadBtn.disabled = false;
|
||
}
|
||
});
|
||
|
||
xhr.addEventListener('error', () => {
|
||
showAlert('Ошибка сети при загрузке', 'danger');
|
||
uploadBtn.disabled = false;
|
||
});
|
||
|
||
xhr.open('POST', `/api/client/tasks/${currentTaskId}/files?connection_id=${currentConnectionId}`, true);
|
||
xhr.send(formData);
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки файлов:', error);
|
||
showAlert('Ошибка при загрузке файлов', 'danger');
|
||
uploadBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// Скачать файл
|
||
async function downloadFile(taskId, fileId, fileName) {
|
||
if (!currentConnectionId) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/client/tasks/${taskId}/files/${fileId}/download?connection_id=${currentConnectionId}`);
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Ошибка скачивания');
|
||
}
|
||
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = fileName;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
a.remove();
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка скачивания файла:', error);
|
||
showAlert('Ошибка при скачивании файла', 'danger');
|
||
}
|
||
}
|
||
|
||
// Загрузка списка подключений для синхронизации
|
||
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 openSyncModal(taskId, taskTitle) {
|
||
syncTaskId = taskId;
|
||
syncTaskTitle = taskTitle;
|
||
|
||
document.getElementById('syncTaskTitle').textContent = taskTitle;
|
||
|
||
loadTargetConnections();
|
||
|
||
document.getElementById('syncModal').classList.add('active');
|
||
}
|
||
|
||
// Закрыть модальное окно синхронизации
|
||
function closeSyncModal() {
|
||
document.getElementById('syncModal').classList.remove('active');
|
||
syncTaskId = null;
|
||
|
||
document.getElementById('targetService').value = '';
|
||
document.getElementById('newServiceInputs').style.display = 'none';
|
||
document.getElementById('targetApiUrl').value = '';
|
||
document.getElementById('targetApiKey').value = '';
|
||
document.getElementById('syncFiles').checked = true;
|
||
document.getElementById('syncProgress').style.display = 'none';
|
||
document.getElementById('syncBtn').disabled = false;
|
||
document.getElementById('syncStatus').innerHTML = '';
|
||
}
|
||
|
||
// Переключение между сохраненным и новым сервисом
|
||
function toggleTargetServiceInput() {
|
||
const targetService = document.getElementById('targetService').value;
|
||
document.getElementById('newServiceInputs').style.display =
|
||
targetService === 'new' ? 'block' : 'none';
|
||
}
|
||
|
||
// Синхронизация задачи
|
||
async function syncTask() {
|
||
if (!syncTaskId) return;
|
||
|
||
const targetService = document.getElementById('targetService').value;
|
||
const syncFiles = document.getElementById('syncFiles').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,
|
||
sync_files: syncFiles
|
||
};
|
||
|
||
document.getElementById('syncProgress').style.display = 'block';
|
||
document.getElementById('syncBtn').disabled = true;
|
||
document.getElementById('syncStatus').innerHTML = 'Начинаем синхронизацию...';
|
||
|
||
try {
|
||
const response = await fetch(`/api/client/tasks/${syncTaskId}/sync?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('syncProgressBar').style.width = progress + '%';
|
||
document.getElementById('syncPercent').textContent = progress + '%';
|
||
|
||
if (progress >= 100) {
|
||
clearInterval(interval);
|
||
|
||
let statusText = `✅ Задача синхронизирована!`;
|
||
|
||
if (data.data.sync_type === 'created') {
|
||
statusText += `<br>📋 Создана новая задача в целевой системе. ID: ${data.data.target_task_id}`;
|
||
} else if (data.data.sync_type === 'updated') {
|
||
statusText += `<br>🔄 Обновлена существующая задача в целевой системе. ID: ${data.data.target_task_id}`;
|
||
}
|
||
|
||
statusText += `<br>👥 Исполнители: сохранены как в исходной задаче`;
|
||
|
||
if (data.data.assignees && data.data.assignees.length > 0) {
|
||
statusText += `<br>👤 Количество исполнителей: ${data.data.assignees.length}`;
|
||
}
|
||
|
||
if (data.data.synced_files && data.data.synced_files.length > 0) {
|
||
const successCount = data.data.synced_files.filter(f => f.success).length;
|
||
const failCount = data.data.synced_files.filter(f => !f.success).length;
|
||
statusText += `<br>📁 Файлы: ${successCount} синхронизировано, ${failCount} ошибок`;
|
||
|
||
if (failCount > 0) {
|
||
const errors = data.data.synced_files
|
||
.filter(f => !f.success)
|
||
.map(f => f.original_name)
|
||
.join(', ');
|
||
statusText += `<br><small style="color: #e74c3c;">Ошибки: ${errors}</small>`;
|
||
}
|
||
}
|
||
|
||
if (data.data.warnings && data.data.warnings.length > 0) {
|
||
statusText += `<br><small style="color: #f39c12;">⚠️ ${data.data.warnings.join('; ')}</small>`;
|
||
}
|
||
|
||
document.getElementById('syncStatus').innerHTML = statusText;
|
||
|
||
setTimeout(() => {
|
||
closeSyncModal();
|
||
showAlert(`Задача синхронизирована с ${data.data.target_service}`, 'success');
|
||
loadTasks();
|
||
}, 3000);
|
||
}
|
||
}, 200);
|
||
} else {
|
||
showAlert(data.error || 'Ошибка синхронизации задачи', 'danger');
|
||
document.getElementById('syncProgress').style.display = 'none';
|
||
document.getElementById('syncBtn').disabled = false;
|
||
document.getElementById('syncStatus').innerHTML = '';
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка синхронизации:', error);
|
||
showAlert('Ошибка при синхронизации задачи', 'danger');
|
||
document.getElementById('syncProgress').style.display = 'none';
|
||
document.getElementById('syncBtn').disabled = false;
|
||
document.getElementById('syncStatus').innerHTML = '';
|
||
}
|
||
}
|
||
|
||
// Смена страницы
|
||
function changePage(delta) {
|
||
const newPage = currentPage + delta;
|
||
if (newPage >= 0 && newPage * pageSize < totalTasks) {
|
||
currentPage = newPage;
|
||
loadTasks();
|
||
}
|
||
}
|
||
|
||
// Обновление пагинации
|
||
function updatePagination() {
|
||
const totalPages = Math.ceil(totalTasks / pageSize);
|
||
|
||
if (totalPages > 1) {
|
||
document.getElementById('pagination').style.display = 'flex';
|
||
document.getElementById('pageInfo').textContent = `Страница ${currentPage + 1} из ${totalPages}`;
|
||
document.getElementById('prevPage').disabled = currentPage === 0;
|
||
document.getElementById('nextPage').disabled = currentPage >= totalPages - 1;
|
||
} else {
|
||
document.getElementById('pagination').style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Показать уведомление
|
||
function showAlert(message, type) {
|
||
const alert = document.getElementById('alert');
|
||
alert.className = `alert alert-${type} show`;
|
||
alert.innerHTML = message;
|
||
|
||
setTimeout(() => {
|
||
alert.classList.remove('show');
|
||
}, 5000);
|
||
}
|
||
|
||
// Вспомогательные функции
|
||
function getStatusText(status) {
|
||
const statusMap = {
|
||
'assigned': 'Назначена',
|
||
'in_progress': 'В работе',
|
||
'completed': 'Выполнена',
|
||
'overdue': 'Просрочена',
|
||
'rework': 'На доработке'
|
||
};
|
||
return statusMap[status] || status || 'Неизвестно';
|
||
}
|
||
|
||
function formatFileSize(bytes) {
|
||
if (bytes === 0) return '0 Б';
|
||
const k = 1024;
|
||
const sizes = ['Б', 'КБ', 'МБ', 'ГБ'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
}
|
||
|
||
function escapeHtml(unsafe) {
|
||
if (!unsafe) return '';
|
||
return unsafe
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
// Инициализация
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
checkAuth();
|
||
loadSavedConnections();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |