Files
minicrm/public/upravlenie.html
2026-02-25 15:30:06 +05:00

549 lines
21 KiB
HTML
Raw 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>Управление межсервисным взаимодействием</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1, h2 {
color: #333;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input, select, textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
background: #4CAF50;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-right: 10px;
}
button:hover {
background: #45a049;
}
button.danger {
background: #f44336;
}
button.danger:hover {
background: #d32f2f;
}
button.secondary {
background: #2196F3;
}
button.secondary:hover {
background: #1976D2;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
tr:hover {
background-color: #f5f5f5;
}
.badge {
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.badge.organizer {
background: #2196F3;
color: white;
}
.badge.executor {
background: #4CAF50;
color: white;
}
.badge.success {
background: #4CAF50;
color: white;
}
.badge.error {
background: #f44336;
color: white;
}
.badge.warning {
background: #ff9800;
color: white;
}
.actions {
display: flex;
gap: 5px;
}
.sync-status {
margin-top: 20px;
padding: 15px;
background: #e3f2fd;
border-radius: 4px;
}
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: white;
margin: 10% auto;
padding: 20px;
border: 1px solid #888;
width: 50%;
border-radius: 8px;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: black;
}
.tab {
overflow: hidden;
border: 1px solid #ccc;
background-color: #f1f1f1;
border-radius: 4px 4px 0 0;
}
.tab button {
background-color: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
color: #333;
}
.tab button:hover {
background-color: #ddd;
}
.tab button.active {
background-color: #ccc;
}
.tabcontent {
display: none;
padding: 20px;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
}
</style>
</head>
<body>
<div class="container">
<h1>Управление межсервисным взаимодействием</h1>
<div class="tab">
<button class="tablinks active" onclick="openTab(event, 'connections')">Подключения</button>
<button class="tablinks" onclick="openTab(event, 'stats')">Статистика</button>
<button class="tablinks" onclick="openTab(event, 'logs')">Логи синхронизации</button>
</div>
<div id="connections" class="tabcontent" style="display: block;">
<h2>Список подключений</h2>
<button onclick="showAddModal()"> Добавить подключение</button>
<table id="connectionsTable">
<thead>
<tr>
<th>ID</th>
<th>Service ID</th>
<th>Название</th>
<th>Тип</th>
<th>Логин</th>
<th>Локальный пользователь</th>
<th>Синхронизация</th>
<th>Последняя синхр.</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="connectionsList"></tbody>
</table>
</div>
<div id="stats" class="tabcontent">
<h2>Статистика синхронизации</h2>
<div id="statsContent" class="sync-status">Загрузка...</div>
</div>
<div id="logs" class="tabcontent">
<h2>Логи синхронизации</h2>
<div id="logsContent"></div>
</div>
</div>
<!-- Модальное окно добавления/редактирования -->
<div id="connectionModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal()">&times;</span>
<h2 id="modalTitle">Добавление подключения</h2>
<form id="connectionForm">
<input type="hidden" id="editId">
<div class="form-group">
<label for="service_id">Service ID (1-4062):</label>
<input type="number" id="service_id" min="1" max="4062" required>
</div>
<div class="form-group">
<label for="service_name">Название сервиса:</label>
<input type="text" id="service_name" required>
</div>
<div class="form-group">
<label for="service_type">Тип сервиса:</label>
<select id="service_type" required>
<option value="organizer">Организатор</option>
<option value="executor">Исполнитель</option>
</select>
</div>
<div class="form-group">
<label for="login">Логин для API:</label>
<input type="text" id="login" required>
</div>
<div class="form-group">
<label for="password">Пароль для API:</label>
<input type="password" id="password" required>
</div>
<div class="form-group" id="apiUrlGroup">
<label for="api_url">API URL организатора:</label>
<input type="url" id="api_url" placeholder="https://example.com">
</div>
<div class="form-group">
<label for="local_user_id">Локальный пользователь (ID):</label>
<input type="number" id="local_user_id">
</div>
<div class="form-group">
<label for="local_user_login">Логин локального пользователя:</label>
<input type="text" id="local_user_login">
</div>
<div class="form-group">
<label for="sync_direction">Направление синхронизации:</label>
<select id="sync_direction">
<option value="outgoing">Отправка статусов</option>
<option value="incoming">Получение задач</option>
<option value="both">Двусторонняя</option>
</select>
</div>
<div class="form-group">
<label for="sync_interval">Интервал синхронизации (минут):</label>
<input type="number" id="sync_interval" min="1" max="1440" value="60">
</div>
<div class="form-group">
<label>
<input type="checkbox" id="sync_enabled" checked> Включить синхронизацию
</label>
</div>
<div class="actions">
<button type="submit">Сохранить</button>
<button type="button" class="danger" onclick="closeModal()">Отмена</button>
</div>
</form>
</div>
</div>
<script>
// Загрузка списка подключений
async function loadConnections() {
try {
const response = await fetch('/api/upravlenie/connections');
const connections = await response.json();
const tbody = document.getElementById('connectionsList');
tbody.innerHTML = '';
connections.forEach(conn => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${conn.id}</td>
<td><strong>${conn.service_id}</strong></td>
<td>${conn.service_name}</td>
<td><span class="badge ${conn.service_type}">${conn.service_type === 'organizer' ? 'Организатор' : 'Исполнитель'}</span></td>
<td>${conn.login}</td>
<td>${conn.local_user_login || conn.local_user_id || '-'}</td>
<td>
${conn.sync_enabled ?
`<span class="badge success">Активна (${conn.sync_interval} мин)</span>` :
'<span class="badge warning">Отключена</span>'}
</td>
<td>${conn.last_sync_at ? new Date(conn.last_sync_at).toLocaleString() : 'Нет'}</td>
<td>
${conn.last_sync_status === 'success' ?
'<span class="badge success">✓</span>' :
conn.last_sync_status === 'error' ?
'<span class="badge error">✗</span>' :
'<span class="badge warning">?</span>'}
</td>
<td class="actions">
<button class="secondary" onclick="editConnection(${conn.id})">✏️</button>
<button class="secondary" onclick="syncConnection(${conn.id})">🔄</button>
<button class="danger" onclick="deleteConnection(${conn.id})">🗑️</button>
</td>
`;
tbody.appendChild(tr);
});
} catch (error) {
console.error('Ошибка загрузки подключений:', error);
}
}
// Загрузка статистики
async function loadStats() {
try {
const response = await fetch('/api/upravlenie/stats');
const stats = await response.json();
const content = document.getElementById('statsContent');
content.innerHTML = `
<p><strong>Всего подключений:</strong> ${stats.total}</p>
<p><strong>Организаторов:</strong> ${stats.organizers}</p>
<p><strong>Исполнителей:</strong> ${stats.executors}</p>
<h3>Последние синхронизации:</h3>
<ul>
${stats.lastSyncs.map(s => `
<li>${s.service}: ${new Date(s.last_sync).toLocaleString()}
(${s.status === 'success' ? '✓' : s.status === 'error' ? '✗' : '?'})
</li>
`).join('')}
</ul>
${stats.errors.length > 0 ? `
<h3>Ошибки:</h3>
<ul style="color: red;">
${stats.errors.map(e => `
<li>${e.service}: ${e.error} (${new Date(e.time).toLocaleString()})</li>
`).join('')}
</ul>
` : ''}
`;
} catch (error) {
console.error('Ошибка загрузки статистики:', error);
}
}
// Открытие модального окна для добавления
function showAddModal() {
document.getElementById('modalTitle').textContent = 'Добавление подключения';
document.getElementById('connectionForm').reset();
document.getElementById('editId').value = '';
document.getElementById('connectionModal').style.display = 'block';
}
// Редактирование подключения
async function editConnection(id) {
try {
const response = await fetch(`/api/upravlenie/connections/${id}`);
const conn = await response.json();
document.getElementById('modalTitle').textContent = 'Редактирование подключения';
document.getElementById('editId').value = conn.id;
document.getElementById('service_id').value = conn.service_id;
document.getElementById('service_name').value = conn.service_name;
document.getElementById('service_type').value = conn.service_type;
document.getElementById('login').value = conn.login;
document.getElementById('password').value = conn.password;
document.getElementById('api_url').value = conn.api_url || '';
document.getElementById('local_user_id').value = conn.local_user_id || '';
document.getElementById('local_user_login').value = conn.local_user_login || '';
document.getElementById('sync_direction').value = conn.sync_direction;
document.getElementById('sync_interval').value = conn.sync_interval;
document.getElementById('sync_enabled').checked = conn.sync_enabled === 1;
document.getElementById('connectionModal').style.display = 'block';
// Обновляем видимость поля API URL
toggleApiUrlField();
} catch (error) {
console.error('Ошибка загрузки данных:', error);
}
}
// Закрытие модального окна
function closeModal() {
document.getElementById('connectionModal').style.display = 'none';
}
// Ручная синхронизация
async function syncConnection(id) {
if (!confirm('Запустить синхронизацию?')) return;
try {
const response = await fetch(`/api/upravlenie/connections/${id}/sync`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
alert('Синхронизация запущена');
loadConnections();
}
} catch (error) {
alert('Ошибка запуска синхронизации');
console.error(error);
}
}
// Удаление подключения
async function deleteConnection(id) {
if (!confirm('Удалить подключение?')) return;
try {
const response = await fetch(`/api/upravlenie/connections/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
loadConnections();
loadStats();
}
} catch (error) {
alert('Ошибка удаления');
console.error(error);
}
}
// Переключение вкладок
function openTab(evt, tabName) {
const tabcontent = document.getElementsByClassName('tabcontent');
for (let i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = 'none';
}
const tablinks = document.getElementsByClassName('tablinks');
for (let i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(' active', '');
}
document.getElementById(tabName).style.display = 'block';
evt.currentTarget.className += ' active';
if (tabName === 'stats') {
loadStats();
}
}
// Переключение видимости поля API URL в зависимости от типа
function toggleApiUrlField() {
const type = document.getElementById('service_type').value;
const apiUrlGroup = document.getElementById('apiUrlGroup');
if (type === 'executor') {
apiUrlGroup.style.display = 'block';
document.getElementById('api_url').required = true;
} else {
apiUrlGroup.style.display = 'none';
document.getElementById('api_url').required = false;
}
}
// Обработка отправки формы
document.getElementById('connectionForm').addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('editId').value;
const url = id ? `/api/upravlenie/connections/${id}` : '/api/upravlenie/connections';
const method = id ? 'PUT' : 'POST';
const data = {
service_id: parseInt(document.getElementById('service_id').value),
service_name: document.getElementById('service_name').value,
service_type: document.getElementById('service_type').value,
login: document.getElementById('login').value,
password: document.getElementById('password').value,
api_url: document.getElementById('api_url').value || null,
local_user_id: parseInt(document.getElementById('local_user_id').value) || null,
local_user_login: document.getElementById('local_user_login').value || null,
sync_direction: document.getElementById('sync_direction').value,
sync_interval: parseInt(document.getElementById('sync_interval').value),
sync_enabled: document.getElementById('sync_enabled').checked ? 1 : 0
};
try {
const response = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success || result.id) {
closeModal();
loadConnections();
loadStats();
} else {
alert('Ошибка: ' + (result.error || 'Неизвестная ошибка'));
}
} catch (error) {
alert('Ошибка сохранения');
console.error(error);
}
});
// Инициализация
document.getElementById('service_type').addEventListener('change', toggleApiUrlField);
// Загрузка при открытии страницы
window.onload = () => {
loadConnections();
loadStats();
};
</script>
</body>
</html>