320 lines
16 KiB
HTML
320 lines
16 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Рассылки - Домовой Бот{% endblock %}
|
||
|
||
{% block nav_broadcast %}active{% endblock %}
|
||
|
||
{% block extra_style %}
|
||
<style>
|
||
#preview { max-width: 400px; margin-top: 1rem; display: none; }
|
||
#preview img { max-width: 100%; border-radius: 8px; }
|
||
.smart-options { display: none; background: #f0f7ff; padding: 20px; border-radius: 8px; margin-top: 15px; border: 2px solid #667eea; }
|
||
.smart-options.active { display: block; }
|
||
.broadcast-type-selector { display: flex; gap: 15px; margin-bottom: 20px; }
|
||
.broadcast-type-option { flex: 1; padding: 20px; border: 2px solid #e2e8f0; border-radius: 8px; cursor: pointer; transition: all 0.3s; text-align: center; }
|
||
.broadcast-type-option:hover { border-color: #667eea; background: #f8f9ff; }
|
||
.broadcast-type-option.active { border-color: #667eea; background: #eef2ff; }
|
||
.broadcast-type-option h4 { margin-bottom: 10px; }
|
||
.broadcast-type-option .icon { font-size: 2.5em; margin-bottom: 10px; }
|
||
.stats-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; margin-top: 30px; }
|
||
.stat-mini { background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center; }
|
||
.stat-mini h3 { font-size: 0.85em; color: #666; margin-bottom: 5px; }
|
||
.stat-mini .value { font-size: 1.8em; font-weight: bold; color: #667eea; }
|
||
.recent-broadcasts { margin-top: 30px; }
|
||
.broadcast-item { background: white; padding: 15px; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; justify-content: space-between; align-items: center; }
|
||
.broadcast-item .text { flex: 1; }
|
||
.broadcast-item .meta { text-align: right; color: #666; font-size: 0.9em; }
|
||
.badge-smart { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
|
||
.badge-regular { background: #6c757d; color: white; }
|
||
.progress-mini { width: 100px; height: 8px; background: #e2e8f0; border-radius: 4px; overflow: hidden; display: inline-block; vertical-align: middle; margin-left: 5px; }
|
||
.progress-mini-fill { height: 100%; background: linear-gradient(90deg, #48bb78, #38a169); }
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<h2 class="mb-4"><i class="bi bi-broadcast"></i> Рассылки</h2>
|
||
|
||
<!-- Выбор типа рассылки -->
|
||
<div class="card mb-4">
|
||
<div class="card-header bg-primary text-white">
|
||
<h5 class="mb-0"><i class="bi bi-send"></i> Создать рассылку</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<!-- Тип рассылки -->
|
||
<label class="form-label fw-bold">Тип рассылки:</label>
|
||
<div class="broadcast-type-selector">
|
||
<div class="broadcast-type-option active" onclick="selectBroadcastType('regular')" id="typeRegular">
|
||
<div class="icon">📢</div>
|
||
<h4>Обычная</h4>
|
||
<p class="text-muted small">Простая рассылка всем получателям</p>
|
||
</div>
|
||
<div class="broadcast-type-option" onclick="selectBroadcastType('smart')" id="typeSmart">
|
||
<div class="icon">✨</div>
|
||
<h4>Умная</h4>
|
||
<p class="text-muted small">С отслеживанием прочтений и статистикой</p>
|
||
</div>
|
||
</div>
|
||
|
||
<form id="broadcastForm" enctype="multipart/form-data">
|
||
<input type="hidden" id="broadcastType" value="regular">
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Текст рассылки</label>
|
||
<textarea class="form-control" id="broadcastText" rows="5"
|
||
placeholder="Введите текст рассылки..." required></textarea>
|
||
<small class="text-muted">Поддерживается HTML: <b>жирный</b>, <i>курсив</i>, <code>код</code></small>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Картинка (необязательно)</label>
|
||
<input type="file" class="form-control" id="broadcastPhoto" accept="image/*" onchange="previewImage()">
|
||
<small class="text-muted">JPG, PNG до 5MB</small>
|
||
<div id="preview"><img id="previewImg" alt="Предпросмотр"></div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Кому отправить:</label>
|
||
<select class="form-select" id="broadcastRecipients">
|
||
<option value="all_and_chat">✅ Всем верифицированным + в чат (по умолчанию)</option>
|
||
<option value="all_verified">✅ Только верифицированным в личку</option>
|
||
<option value="ig_only">👥 Только Инициативной Группе</option>
|
||
<option value="chat_only">💬 Только в общий чат</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Опции умной рассылки -->
|
||
<div class="smart-options" id="smartOptions">
|
||
<h5 class="mb-3"><i class="bi bi-stars"></i> Настройки умной рассылки</h5>
|
||
|
||
<div class="form-check mb-3">
|
||
<input class="form-check-input" type="checkbox" id="hasReadButton" checked>
|
||
<label class="form-check-label" for="hasReadButton">
|
||
Добавить кнопку "✅ Прочитал"
|
||
</label>
|
||
<div class="text-muted small">Жильцы смогут отметить что прочитали сообщение</div>
|
||
</div>
|
||
|
||
<div class="form-check mb-3">
|
||
<input class="form-check-input" type="checkbox" id="trackReads" checked>
|
||
<label class="form-check-label" for="trackReads">
|
||
Отслеживать прочтения
|
||
</label>
|
||
<div class="text-muted small">В статистике будет видно кто прочитал, а кто нет</div>
|
||
</div>
|
||
|
||
<div class="alert alert-info mb-0">
|
||
<i class="bi bi-info-circle"></i>
|
||
<b>После отправки умной рассылки вы сможете:</b>
|
||
<ul class="mb-0 mt-2">
|
||
<li>Посмотреть кто прочитал сообщение</li>
|
||
<li>Увидеть кто НЕ прочитал</li>
|
||
<li>Отправить напоминание непрочитавшим</li>
|
||
<li>Просмотреть общую статистику в разделе <a href="/broadcasts" target="_blank">Умные рассылки →</a></li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-primary btn-lg mt-3">
|
||
<i class="bi bi-send"></i> <span id="submitBtnText">Отправить рассылку</span>
|
||
</button>
|
||
</form>
|
||
|
||
<div id="result" class="mt-4"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Краткая статистика -->
|
||
<div class="stats-summary" id="statsSummary" style="display:none;">
|
||
<div class="stat-mini">
|
||
<h3>Всего рассылок</h3>
|
||
<div class="value" id="statTotal">0</div>
|
||
</div>
|
||
<div class="stat-mini">
|
||
<h3>Умных рассылок</h3>
|
||
<div class="value" id="statSmart">0</div>
|
||
</div>
|
||
<div class="stat-mini">
|
||
<h3>Средний % прочтений</h3>
|
||
<div class="value" id="statReadPercent">0%</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Последние рассылки -->
|
||
<div class="recent-broadcasts" id="recentBroadcasts" style="display:none;">
|
||
<h5 class="mb-3">📊 Последние рассылки</h5>
|
||
<div id="broadcastsList"></div>
|
||
<a href="/broadcasts" class="btn btn-outline-primary mt-2">
|
||
<i class="bi bi-arrow-right"></i> Полная статистика умных рассылок
|
||
</a>
|
||
</div>
|
||
|
||
<div class="card mt-4">
|
||
<div class="card-body">
|
||
<h5>Как использовать:</h5>
|
||
<ol>
|
||
<li>Выберите тип: <b>Обычная</b> или <b>Умная</b> рассылка</li>
|
||
<li>Введите текст объявления</li>
|
||
<li>Прикрепите картинку (необязательно)</li>
|
||
<li>Выберите получателей</li>
|
||
<li>Для умной рассылки настройте опции отслеживания</li>
|
||
<li>Нажмите "Отправить рассылку"</li>
|
||
</ol>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_script %}
|
||
<script>
|
||
// Выбор типа рассылки
|
||
function selectBroadcastType(type) {
|
||
document.getElementById('broadcastType').value = type;
|
||
document.getElementById('typeRegular').classList.toggle('active', type === 'regular');
|
||
document.getElementById('typeSmart').classList.toggle('active', type === 'smart');
|
||
document.getElementById('smartOptions').classList.toggle('active', type === 'smart');
|
||
document.getElementById('submitBtnText').textContent = type === 'smart' ? '✨ Отправить умную рассылку' : '📢 Отправить рассылку';
|
||
}
|
||
|
||
// Предпросмотр изображения
|
||
function previewImage() {
|
||
const file = document.getElementById('broadcastPhoto').files[0];
|
||
const preview = document.getElementById('preview');
|
||
const previewImg = document.getElementById('previewImg');
|
||
|
||
if (file) {
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
previewImg.src = e.target.result;
|
||
preview.style.display = 'block';
|
||
}
|
||
reader.readAsDataURL(file);
|
||
} else {
|
||
preview.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Обновление информации о получателях
|
||
document.getElementById('broadcastRecipients').addEventListener('change', function() {
|
||
const info = {
|
||
'all_verified': 'Все верифицированные пользователи (в личку)',
|
||
'ig_only': 'Только участники Инициативной Группы (в личку)',
|
||
'chat_only': 'Только в общий чат (одно сообщение)',
|
||
'all_and_chat': 'Все верифицированные (в личку) + общий чат'
|
||
};
|
||
// Можно добавить отображение info где-то на странице
|
||
});
|
||
|
||
// Отправка формы
|
||
document.getElementById('broadcastForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
|
||
const type = document.getElementById('broadcastType').value;
|
||
const text = document.getElementById('broadcastText').value;
|
||
const photo = document.getElementById('broadcastPhoto').files[0];
|
||
const recipients = document.getElementById('broadcastRecipients').value;
|
||
const resultDiv = document.getElementById('result');
|
||
|
||
if (!text.trim()) {
|
||
alert('Введите текст рассылки!');
|
||
return;
|
||
}
|
||
|
||
const typeLabel = type === 'smart' ? 'умную' : '';
|
||
if (!confirm(`Отправить${typeLabel ? ' ' + typeLabel : ''} рассылку выбранным получателям?`)) return;
|
||
|
||
resultDiv.innerHTML = '<div class="alert alert-info">📤 Отправка рассылки...</div>';
|
||
|
||
const formData = new FormData();
|
||
formData.append('text', text);
|
||
formData.append('recipients', recipients);
|
||
formData.append('broadcast_type', type);
|
||
|
||
if (type === 'smart') {
|
||
formData.append('has_read_button', document.getElementById('hasReadButton').checked);
|
||
formData.append('track_reads', document.getElementById('trackReads').checked);
|
||
}
|
||
|
||
if (photo) {
|
||
formData.append('photo', photo);
|
||
}
|
||
|
||
try {
|
||
const endpoint = type === 'smart' ? '/api/broadcast/create' : '/api/broadcast/send';
|
||
const r = await fetch(endpoint, { method: 'POST', body: formData });
|
||
const result = await r.json();
|
||
|
||
if (result.success) {
|
||
let message = `✅ ${result.message}`;
|
||
if (type === 'smart') {
|
||
message += '<br><br>📊 <a href="/broadcasts">Посмотреть статистику прочтений →</a>';
|
||
}
|
||
resultDiv.innerHTML = `<div class="alert alert-success">${message}</div>`;
|
||
document.getElementById('broadcastForm').reset();
|
||
document.getElementById('preview').style.display = 'none';
|
||
selectBroadcastType('regular');
|
||
|
||
// Загружаем статистику
|
||
loadBroadcastStats();
|
||
} else {
|
||
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${result.error || 'Неизвестная'}</div>`;
|
||
}
|
||
} catch (err) {
|
||
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${err.message}</div>`;
|
||
}
|
||
});
|
||
|
||
// Загрузка статистики рассылок
|
||
async function loadBroadcastStats() {
|
||
try {
|
||
const r = await fetch('/api/broadcasts/list?limit=5');
|
||
const result = await r.json();
|
||
|
||
if (result.success && result.broadcasts) {
|
||
const broadcasts = result.broadcasts;
|
||
|
||
// Показываем блок статистики
|
||
document.getElementById('statsSummary').style.display = 'grid';
|
||
document.getElementById('recentBroadcasts').style.display = 'block';
|
||
|
||
// Заполняем цифры
|
||
document.getElementById('statTotal').textContent = result.total || broadcasts.length;
|
||
const smartCount = broadcasts.filter(b => b.broadcast_type === 'smart').length;
|
||
document.getElementById('statSmart').textContent = smartCount;
|
||
|
||
const avgRead = broadcasts.length > 0
|
||
? (broadcasts.reduce((sum, b) => sum + (b.read_percent || 0), 0) / broadcasts.length).toFixed(1)
|
||
: 0;
|
||
document.getElementById('statReadPercent').textContent = avgRead + '%';
|
||
|
||
// Список последних
|
||
const listDiv = document.getElementById('broadcastsList');
|
||
listDiv.innerHTML = broadcasts.map(b => `
|
||
<div class="broadcast-item">
|
||
<div class="text">
|
||
<span class="badge ${b.broadcast_type === 'smart' ? 'badge-smart' : 'badge-regular'}">
|
||
${b.broadcast_type === 'smart' ? '✨ Умная' : '📢 Обычная'}
|
||
</span>
|
||
${b.text.substring(0, 80)}${b.text.length > 80 ? '...' : ''}
|
||
${b.broadcast_type === 'smart' ? `
|
||
<div class="progress-mini">
|
||
<div class="progress-mini-fill" style="width: ${b.read_percent}%"></div>
|
||
</div>
|
||
<small class="text-muted ms-2">${b.read_percent}% прочитали</small>
|
||
` : ''}
|
||
</div>
|
||
<div class="meta">
|
||
<div>${b.sent_at || '-'}</div>
|
||
<div>Отпр: ${b.total_sent || 0}</div>
|
||
${b.broadcast_type === 'smart' ? `<div><a href="/broadcasts">📊 Статистика</a></div>` : ''}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
} catch (err) {
|
||
console.error('Ошибка загрузки статистики:', err);
|
||
}
|
||
}
|
||
|
||
// Загружаем статистику при загрузке страницы
|
||
document.addEventListener('DOMContentLoaded', loadBroadcastStats);
|
||
</script>
|
||
{% endblock %}
|