domovoy_bot/web/templates/broadcasts.html
Admin 222c2ba521 🚀 feat: V3.1 - Smart broadcasts with read tracking + Weekly digests with approval workflow
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 23:01:10 +04:00

195 lines
9.8 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>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; color: #333; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 10px; margin-bottom: 30px; }
.header h1 { font-size: 2em; margin-bottom: 10px; }
.back-link { color: white; text-decoration: none; opacity: 0.9; }
.back-link:hover { opacity: 1; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
.stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.stat-card h3 { font-size: 0.9em; color: #666; margin-bottom: 5px; }
.stat-card .value { font-size: 2em; font-weight: bold; color: #667eea; }
table { width: 100%; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
th { background: #667eea; color: white; padding: 15px; text-align: left; }
td { padding: 15px; border-bottom: 1px solid #eee; }
tr:hover { background: #f8f9fa; }
.btn { padding: 8px 16px; border: none; border-radius: 5px; cursor: pointer; font-size: 0.9em; transition: all 0.2s; }
.btn-primary { background: #667eea; color: white; }
.btn-primary:hover { background: #5568d3; }
.btn-success { background: #48bb78; color: white; }
.btn-success:hover { background: #38a169; }
.btn-warning { background: #ed8936; color: white; }
.btn-warning:hover { background: #dd6b20; }
.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; }
.badge-success { background: #c6f6d5; color: #22543d; }
.badge-warning { background: #feebc8; color: #7c2d12; }
.badge-info { background: #bee3f8; color: #2c5282; }
.progress-bar { width: 100%; height: 20px; background: #e2e8f0; border-radius: 10px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #48bb78, #38a169); transition: width 0.3s; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; }
.modal.active { display: flex; align-items: center; justify-content: center; }
.modal-content { background: white; padding: 30px; border-radius: 10px; max-width: 800px; width: 90%; max-height: 80vh; overflow-y: auto; }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.close-btn { background: none; border: none; font-size: 1.5em; cursor: pointer; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<a href="/" class="back-link">На главную</a>
<h1>📢 Умные рассылки</h1>
<p>Отслеживание прочтений и статистика</p>
</div>
<div class="stats">
<div class="stat-card">
<h3>Всего рассылок</h3>
<div class="value">{{ broadcasts|length }}</div>
</div>
<div class="stat-card">
<h3>Всего отправлено</h3>
<div class="value">{{ broadcasts|sum(attribute='total_sent') }}</div>
</div>
<div class="stat-card">
<h3>Всего прочитано</h3>
<div class="value">{{ broadcasts|sum(attribute='read_count') }}</div>
</div>
<div class="stat-card">
<h3>Средний % прочтений</h3>
<div class="value">{% if broadcasts|length > 0 %}{{ (broadcasts|sum(attribute='read_percent') / broadcasts|length)|round(1) }}%{% else %}0%{% endif %}</div>
</div>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Текст</th>
<th>Дата</th>
<th>Отправлено</th>
<th>Прочитано</th>
<th>Не прочитано</th>
<th>% прочтений</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for b in broadcasts %}
<tr>
<td>#{{ b.id }}</td>
<td>{{ b.text }}</td>
<td>{{ b.sent_at }}</td>
<td>{{ b.total_sent }}</td>
<td><span class="badge badge-success">{{ b.read_count }}</span></td>
<td><span class="badge badge-warning">{{ b.unread_count }}</span></td>
<td>
<div class="progress-bar">
<div class="progress-fill" style="width: {{ b.read_percent }}%"></div>
</div>
<small>{{ b.read_percent }}%</small>
</td>
<td>
<button class="btn btn-primary" onclick="showReaders({{ b.id }})">👥 Кто прочитал</button>
<button class="btn btn-warning" onclick="showNonReaders({{ b.id }})">⚠️ Кто НЕ прочитал</button>
{% if not b.is_reminder_sent %}
<button class="btn btn-success" onclick="sendReminder({{ b.id }})">📬 Напоминание</button>
{% else %}
<span class="badge badge-info">Напоминание отправлено</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if broadcasts|length == 0 %}
<div style="text-align: center; padding: 50px; background: white; border-radius: 8px; margin-top: 20px;">
<h2>📭 Рассылок пока нет</h2>
<p style="margin-top: 10px;">Используйте команду /broadcast в Telegram для создания рассылки</p>
</div>
{% endif %}
</div>
<!-- Модальное окно для списка прочитавших -->
<div id="readersModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>👥 Прочитавшие</h2>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div id="readersList"></div>
</div>
</div>
<!-- Модальное окно для списка непрочитавших -->
<div id="nonReadersModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>⚠️ Не прочитавшие</h2>
<button class="close-btn" onclick="closeNonReadersModal()">&times;</button>
</div>
<div id="nonReadersList"></div>
</div>
</div>
<script>
function showReaders(broadcastId) {
fetch(`/api/broadcast/readers/${broadcastId}`)
.then(r => r.json())
.then(data => {
const list = document.getElementById('readersList');
if (data.readers.length === 0) {
list.innerHTML = '<p>Пока никто не прочитал</p>';
} else {
list.innerHTML = '<table><tr><th>Имя</th><th>Квартира</th><th>Время прочтения</th></tr>' +
data.readers.map(r => `<tr><td>${r.name}</td><td>${r.apartment || '-'}</td><td>${r.read_at}</td></tr>`).join('') +
'</table>';
}
document.getElementById('readersModal').classList.add('active');
});
}
function closeModal() {
document.getElementById('readersModal').classList.remove('active');
}
function showNonReaders(broadcastId) {
fetch(`/api/broadcast/non_readers/${broadcastId}`)
.then(r => r.json())
.then(data => {
const list = document.getElementById('nonReadersList');
if (data.non_readers.length === 0) {
list.innerHTML = '<p>✅ Все прочитали!</p>';
} else {
list.innerHTML = '<table><tr><th>Имя</th><th>Квартира</th></tr>' +
data.non_readers.map(r => `<tr><td>${r.name}</td><td>${r.apartment || '-'}</td></tr>`).join('') +
'</table>';
}
document.getElementById('nonReadersModal').classList.add('active');
});
}
function closeNonReadersModal() {
document.getElementById('nonReadersModal').classList.remove('active');
}
function sendReminder(broadcastId) {
if (confirm('Отправить напоминание непрочитавшим?')) {
fetch(`/api/broadcast/send_reminder/${broadcastId}`, { method: 'POST' })
.then(r => r.json())
.then(data => {
alert(data.message);
location.reload();
});
}
}
</script>
</body>
</html>