Files
minicrm/public/client.html
2026-03-09 14:20:28 +05:00

1021 lines
30 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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()">&times;</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()">&times;</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>
<!-- Модальное окно импорта задачи -->
<div class="modal" id="import-task-modal">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h3>Копирование задачи в локальную CRM</h3>
<span class="modal-close" onclick="closeImportModal()">&times;</span>
</div>
<div class="modal-body">
<p><strong>Задача:</strong> <span id="import-task-title"></span></p>
<p><strong>Описание:</strong> <span id="import-task-description"></span></p>
<div class="form-group">
<label for="import-due-date">Дата выполнения:</label>
<input type="datetime-local" id="import-due-date" class="form-control" required>
</div>
<div class="form-group">
<label>Выберите исполнителей (локальные пользователи):</label>
<div class="users-checklist" id="import-users-checklist" style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;"></div>
</div>
<div class="modal-footer" style="margin-top: 20px;">
<button class="btn btn-secondary" onclick="closeImportModal()">Отмена</button>
<button class="btn btn-success" onclick="confirmImport()">✅ Импортировать</button>
</div>
</div>
</div>
</div>
<script src="client.js"></script>
</body>
</html>