domovoy_bot/web/templates/broadcast.html
Admin fa8c1d53d9 🔄 feat: Unify Regular and Smart broadcast UI into single page with type selector
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 23:29:32 +04:00

320 lines
16 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.

{% 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 %}