connected btx (no import)

This commit is contained in:
Ivan I. Ovchinnikov
2026-03-18 22:05:02 +03:00
parent f0fdba0ff6
commit d1570b30c4
17 changed files with 1914 additions and 602 deletions
+137
View File
@@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Результаты импорта — Bitmine</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container { max-width: 800px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 24px;
margin-bottom: 20px;
}
.summary {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.summary-item {
flex: 1;
padding: 16px;
border-radius: 8px;
text-align: center;
}
.summary-success {
background: #e8f5e9;
color: #388e3c;
}
.summary-error {
background: #ffebee;
color: #c62828;
}
.summary-count {
font-size: 32px;
font-weight: 700;
}
.summary-label {
font-size: 14px;
margin-top: 4px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
th { background: #f8f9fa; }
.status-success { color: #388e3c; }
.status-error { color: #c62828; }
.btn {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
text-decoration: none;
display: inline-block;
}
.btn:hover { background: #0056b3; }
</style>
</head>
<body>
<div class="container">
<h1>📊 Результаты импорта</h1>
<div class="card">
<div class="summary">
<div class="summary-item summary-success">
<div class="summary-count">{{ success_count }}</div>
<div class="summary-label">Успешно</div>
</div>
<div class="summary-item summary-error">
<div class="summary-count">{{ error_count }}</div>
<div class="summary-label">Ошибок</div>
</div>
</div>
</div>
<div class="card">
<h2>Детали</h2>
<table>
<thead>
<tr>
<th>Redmine #</th>
<th>Задача</th>
<th>Bitrix24 #</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
{% for result in results %}
<tr>
<td>{{ result.task_id }}</td>
<td>{{ result.task_title }}</td>
<td>
{% match result.bitrix_id %}
{% when Some(id) %}
<a href="#" class="status-success">#{{ id }}</a>
{% when None %}
{% endmatch %}
</td>
<td>
{% match result.error %}
{% when Some(err) %}
<span class="status-error">❌ {{ err }}</span>
{% when None %}
<span class="status-success">✅ Успешно</span>
{% endmatch %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card">
<a href="/api/redmine/tasks" class="btn">← Вернуться к задачам</a>
</div>
</div>
</body>
</html>
+187
View File
@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Импорт задач — Bitmine</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container { max-width: 1200px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 24px;
margin-bottom: 20px;
}
.form-group { margin-bottom: 20px; }
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
select {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
background: white;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 16px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
th { background: #f8f9fa; }
.btn {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.btn:hover { background: #0056b3; }
.btn-success { background: #28a745; }
.btn-success:hover { background: #218838; }
.btn-secondary { background: #6c757d; }
.btn-secondary:hover { background: #545b62; }
.error {
background: #ffebee;
color: #c62828;
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #007bff;
text-decoration: none;
}
.select-all {
margin-bottom: 16px;
}
.task-count {
color: #666;
margin-left: 8px;
}
</style>
</head>
<body>
<div class="container">
<a href="/api/redmine/tasks" class="back-link">← Назад к списку задач</a>
<h1>📥 Импорт задач из Redmine в Bitrix24</h1>
{% if error.is_some() %}
<div class="error">{{ error.as_ref().unwrap() }}</div>
{% endif %}
<form method="POST" action="/import">
<div class="card">
<div class="form-group">
<label for="project_id">📁 Выберите проект в Bitrix24 для импорта:</label>
<select name="project_id" id="project_id" required>
<option value="">-- Выберите проект --</option>
{% for project in projects %}
<option value="{{ project.id }}">{{ project.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="card">
<h2>Выберите задачи для импорта</h2>
<div class="select-all">
<label>
<input type="checkbox" id="select-all" onchange="toggleAll(this)">
Выбрать все <span class="task-count">({{ tasks.len() }} задач)</span>
</label>
</div>
<table>
<thead>
<tr>
<th style="width: 40px;"></th>
<th></th>
<th>Тема</th>
<th>Статус</th>
<th>Проект</th>
</tr>
</thead>
<tbody>
{% for task in tasks %}
<tr>
<td>
<input type="checkbox" name="task_ids" value="{{ task.id }}">
</td>
<td>{{ task.id }}</td>
<td>{{ task.subject }}</td>
<td>
<span class="status status-{{ task.status_class }}" style="padding: 4px 12px; border-radius: 12px; font-size: 12px;">
{{ task.status_name }}
</span>
</td>
<td>
{% match task.project %}
{% when Some(proj) %}{{ proj }}
{% when None %}—
{% endmatch %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card">
<button type="submit" class="btn btn-success">🚀 Импортировать выбранные задачи</button>
<a href="/api/redmine/tasks" class="btn btn-secondary">Отмена</a>
</div>
</form>
</div>
<script>
async function submitImport(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = {
project_id: formData.get('project_id'),
task_ids: Array.from(formData.getAll('task_ids'))
};
const response = await fetch('/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
window.location.href = '/import/result';
}
</script>
<style>
.status-new { background: #e3f2fd; color: #1976d2; }
.status-progress { background: #fff3e0; color: #f57c00; }
.status-resolved { background: #e8f5e9; color: #388e3c; }
.status-closed { background: #f5f5f5; color: #616161; }
</style>
</body>
</html>
+5 -4
View File
@@ -44,6 +44,8 @@
.btn-bitrix:hover { background: #008cc7; }
.btn-settings { background: #6c757d; }
.btn-settings:hover { background: #545b62; }
.btn-success { background: #28a745; }
.btn-success:hover { background: #218838; }
table {
width: 100%;
background: white;
@@ -117,12 +119,11 @@
{% endif %}
<div class="controls">
<a href="/api/redmine/tasks" class="btn {% if !has_settings %}btn-disabled{% endif %}">
📋 Получить задачи из Redmine
</a>
<a href="/api/redmine/tasks" class="btn">📋 Получить задачи из Redmine</a>
<a href="/import" class="btn btn-success">📥 Импорт в Bitrix24</a>
<button class="btn btn-bitrix" disabled>📌 Получить задачи из Bitrix24</button>
</div>
{% if error.is_some() %}
<div class="error">{{ error.as_ref().unwrap() }}</div>
{% endif %}
+219 -25
View File
@@ -11,13 +11,20 @@
background: #f5f5f5;
padding: 20px;
}
.container { max-width: 600px; margin: 0 auto; }
.container { max-width: 800px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 24px;
margin-bottom: 20px;
}
.card h2 {
color: #333;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #eee;
}
.form-group { margin-bottom: 20px; }
label {
@@ -56,11 +63,11 @@
font-weight: 500;
}
.btn:hover { background: #0056b3; }
.btn-secondary {
background: #6c757d;
margin-left: 10px;
}
.btn-secondary { background: #6c757d; margin-left: 10px; }
.btn-secondary:hover { background: #545b62; }
.btn-test { background: #28a745; margin-left: 10px; }
.btn-test:hover { background: #218838; }
.btn-test:disabled { background: #ccc; cursor: not-allowed; }
.error {
background: #ffebee;
color: #c62828;
@@ -90,6 +97,52 @@
font-size: 13px;
color: #e65100;
}
.test-result {
margin-top: 12px;
padding: 12px;
border-radius: 6px;
display: none;
}
.test-result.success {
background: #e8f5e9;
color: #388e3c;
display: block;
}
.test-result.error {
background: #ffebee;
color: #c62828;
display: block;
}
.webhook-instructions {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 16px;
margin-top: 12px;
font-size: 13px;
}
.webhook-instructions ol {
margin-left: 20px;
margin-top: 8px;
}
.webhook-instructions li {
margin-bottom: 8px;
}
.webhook-instructions code {
background: #e9ecef;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}
.saved-indicator {
display: inline-block;
margin-left: 8px;
padding: 4px 8px;
background: #e8f5e9;
color: #388e3c;
border-radius: 4px;
font-size: 12px;
}
</style>
</head>
<body>
@@ -98,16 +151,28 @@
<h1>⚙️ Настройки подключения</h1>
{% if error.is_some() %}
<div class="error">{{ error.as_ref().unwrap() }}</div>
{% endif %}
{% if success.is_some() %}
<div class="success">{{ success.as_ref().unwrap() }}</div>
{% endif %}
<!-- Redmine Settings -->
<div class="card">
{% if error.is_some() %}
<div class="error">{{ error.as_ref().unwrap() }}</div>
{% endif %}
<h2>
🔴 Redmine
{% if !redmine_url.is_empty() %}
<span class="saved-indicator">✓ Сохранено</span>
{% endif %}
</h2>
{% if success.is_some() %}
<div class="success">{{ success.as_ref().unwrap() }}</div>
{% endif %}
<form method="POST" action="/settings">
<form method="POST" action="/settings" id="redmine-form">
<!-- Скрытые поля для сохранения Bitrix настроек -->
<input type="hidden" name="bitrix_url" value="{{ bitrix_url }}">
<input type="hidden" name="bitrix_webhook" value="{{ bitrix_webhook }}">
<div class="form-group">
<label for="redmine_url">Redmine URL</label>
<input
@@ -115,10 +180,9 @@
id="redmine_url"
name="redmine_url"
value="{{ redmine_url }}"
placeholder="https://redmine.example.com"
required
placeholder="https://redmine.company.com"
>
<div class="help-text">Полный URL вашего Redmine, например: https://redmine.company.com</div>
<div class="help-text">Полный URL вашего Redmine</div>
</div>
<div class="form-group">
@@ -128,10 +192,12 @@
id="redmine_api_key"
name="redmine_api_key"
value=""
placeholder="Ваш API ключ из настроек Redmine"
required
placeholder="Введите API ключ для сохранения"
>
<div class="help-text">Ключ можно получить в Настройки → Мой аккаунт → API access key</div>
{% if !redmine_url.is_empty() %}
<div class="help-text" style="color: #388e3c;">✓ API ключ уже сохранён в сессии</div>
{% endif %}
</div>
<div class="form-group">
@@ -142,20 +208,148 @@
name="redmine_user_id"
value="{{ redmine_user_id }}"
placeholder="7"
required
>
<div class="help-text">Ваш ID пользователя в Redmine (видно в профиле или URL)</div>
</div>
<button type="submit" class="btn">💾 Сохранить настройки</button>
<a href="/" class="btn btn-secondary">Отмена</a>
<button type="submit" class="btn">💾 Сохранить Redmine</button>
<button type="button" class="btn btn-test" onclick="testRedmineConnection()">🔍 Тест подключения</button>
<div id="redmine-test-result" class="test-result"></div>
</form>
</div>
<!-- Bitrix24 Settings -->
<div class="card">
<h2>
🔵 Bitrix24
{% if !bitrix_url.is_empty() %}
<span class="saved-indicator">✓ Сохранено</span>
{% endif %}
</h2>
<div class="security-note">
🔒 <strong>Безопасность:</strong> Ваши учётные данные хранятся только в сессии браузера и не сохраняются на сервере.
При закрытии браузера сессия истекает и данные удаляются.
</div>
<form method="POST" action="/settings" id="bitrix-form">
<!-- Скрытые поля для сохранения Redmine настроек -->
<input type="hidden" name="redmine_url" value="{{ redmine_url }}">
<input type="hidden" name="redmine_user_id" value="{{ redmine_user_id }}">
<div class="form-group">
<label for="bitrix_url">Bitrix24 URL</label>
<input
type="text"
id="bitrix_url"
name="bitrix_url"
value="{{ bitrix_url }}"
placeholder="https://corp.company.com"
>
<div class="help-text">URL вашего портала Bitrix24</div>
</div>
<div class="form-group">
<label for="bitrix_webhook">Входящий вебхук</label>
<input
type="text"
id="bitrix_webhook"
name="bitrix_webhook"
value="{{ bitrix_webhook }}"
placeholder="rest/105/abc123xyz/"
>
<div class="help-text">Код вебхука из раздела Разработчикам</div>
{% if !bitrix_url.is_empty() %}
<div class="help-text" style="color: #388e3c;">✓ Вебхук уже сохранён в сессии</div>
{% endif %}
</div>
<div class="webhook-instructions">
<strong>📋 Как получить входящий вебхук в Bitrix24:</strong>
<ol>
<li>Откройте ваш Bitrix24 портал</li>
<li>Перейдите в меню <code>Разработчикам</code> (внизу левого меню)</li>
<li>Выберите <code>Другое</code><code>Входящий вебхук</code></li>
<li>Выберите права доступа: <code>tasks</code>, <code>user</code>, <code>department</code></li>
<li>Нажмите <code>Создать вебхук</code></li>
<li>Скопируйте URL вида <code>https://your-portal.com/rest/105/CODE/</code></li>
<li>В поле выше вставьте только часть после домена: <code>rest/105/CODE/</code></li>
</ol>
</div>
<button type="submit" class="btn">💾 Сохранить Bitrix24</button>
<button type="button" class="btn btn-test" onclick="testBitrixConnection()">🔍 Тест подключения</button>
<div id="bitrix-test-result" class="test-result"></div>
</form>
</div>
<div class="security-note">
🔒 <strong>Безопасность:</strong> Ваши учётные данные хранятся только в сессии браузера и не сохраняются на сервере.
При закрытии браузера сессия истекает и данные удаляются.
</div>
</div>
<script>
async function testRedmineConnection() {
const resultDiv = document.getElementById('redmine-test-result');
const btn = event.target;
btn.disabled = true;
btn.textContent = '⏳ Проверка...';
resultDiv.className = 'test-result';
resultDiv.textContent = '';
try {
const response = await fetch('/api/test/redmine', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const data = await response.json();
if (data.success) {
resultDiv.className = 'test-result success';
resultDiv.textContent = '✅ ' + data.message;
} else {
resultDiv.className = 'test-result error';
resultDiv.textContent = '❌ ' + data.message;
}
} catch (e) {
resultDiv.className = 'test-result error';
resultDiv.textContent = '❌ Ошибка: ' + e.message;
} finally {
btn.disabled = false;
btn.textContent = '🔍 Тест подключения';
}
}
async function testBitrixConnection() {
const resultDiv = document.getElementById('bitrix-test-result');
const btn = event.target;
btn.disabled = true;
btn.textContent = '⏳ Проверка...';
resultDiv.className = 'test-result';
resultDiv.textContent = '';
try {
const response = await fetch('/api/test/bitrix', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const data = await response.json();
if (data.success) {
resultDiv.className = 'test-result success';
resultDiv.textContent = '✅ ' + data.message;
} else {
resultDiv.className = 'test-result error';
resultDiv.textContent = '❌ ' + data.message;
}
} catch (e) {
resultDiv.className = 'test-result error';
resultDiv.textContent = '❌ Ошибка: ' + e.message;
} finally {
btn.disabled = false;
btn.textContent = '🔍 Тест подключения';
}
}
</script>
</body>
</html>