domovoy_bot/web/templates/broadcasts.html
Admin f7aaf96cfa 🎨 feat: Add sidebar navigation for Smart Broadcasts and Digests sections
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 23:21:31 +04:00

301 lines
15 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_smart_broadcast %}active{% endblock %}
{% block extra_style %}
* { margin: 0; padding: 0; box-sizing: border-box; }
{% endblock %}
{% block content %}
<style>
.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; }
.create-form { background: white; padding: 25px; border-radius: 10px; margin-bottom: 30px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.create-form h2 { color: #667eea; margin-bottom: 15px; }
.form-group { margin-bottom: 20px; }
.form-group label { display: block; font-weight: 500; margin-bottom: 5px; }
.form-group textarea { width: 100%; min-height: 120px; padding: 12px; border: 2px solid #e2e8f0; border-radius: 5px; font-family: inherit; font-size: 1em; resize: vertical; }
.form-group textarea:focus { outline: none; border-color: #667eea; }
.form-group select, .form-group input[type="file"] { width: 100%; padding: 10px; border: 2px solid #e2e8f0; border-radius: 5px; font-size: 1em; }
.checkbox-group { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.checkbox-group input[type="checkbox"] { width: 20px; height: 20px; }
.btn { padding: 12px 24px; border: none; border-radius: 5px; cursor: pointer; font-size: 1em; 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; }
.btn-info { background: #4299e1; color: white; }
.btn-info:hover { background: #3182ce; }
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; }
.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; }
.status-msg { padding: 15px; border-radius: 5px; margin: 15px 0; display: none; }
.status-msg.success { background: #c6f6d5; color: #22543d; border: 1px solid #9ae6b4; }
.status-msg.error { background: #fed7d7; color: #742a2a; border: 1px solid #feb2b2; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<a href="/" class="back-link">На главную</a>
<h1>📢 Умные рассылки</h1>
<p>Создание рассылок с отслеживанием прочтений</p>
</div>
<!-- Форма создания рассылки -->
<div class="create-form">
<h2>✨ Создать новую рассылку</h2>
<div id="statusMsg" class="status-msg"></div>
<div class="form-group">
<label for="broadcastText">Текст рассылки *</label>
<textarea id="broadcastText" placeholder="Введите текст рассылки. Можно использовать HTML: <b>жирный</b>, <i>курсив</i>, <code>код</code>"></textarea>
</div>
<div class="form-group">
<label for="broadcastPhoto">Фото (необязательно)</label>
<input type="file" id="broadcastPhoto" accept="image/*">
</div>
<div class="form-group">
<label for="broadcastRecipients">Получатели *</label>
<select id="broadcastRecipients">
<option value="all_and_chat">💬 В чат + всем верифицированным в личку</option>
<option value="all_verified">✅ Только верифицированным в личку</option>
<option value="ig_only">👥 Только инициативной группе</option>
</select>
</div>
<div class="checkbox-group">
<input type="checkbox" id="hasReadButton" checked>
<label for="hasReadButton">Добавить кнопку "✅ Прочитал"</label>
</div>
<button class="btn btn-primary" onclick="sendBroadcast()">📢 Отправить рассылку</button>
</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-info" 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;">Создайте первую рассылку формой выше!</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>
async function sendBroadcast() {
const text = document.getElementById('broadcastText').value.trim();
const photo = document.getElementById('broadcastPhoto').files[0];
const recipients = document.getElementById('broadcastRecipients').value;
const hasReadButton = document.getElementById('hasReadButton').checked;
const statusMsg = document.getElementById('statusMsg');
if (!text) {
statusMsg.textContent = '❌ Введите текст рассылки!';
statusMsg.className = 'status-msg error';
statusMsg.style.display = 'block';
return;
}
const formData = new FormData();
formData.append('text', text);
formData.append('recipients', recipients);
formData.append('has_read_button', hasReadButton);
if (photo) {
formData.append('photo', photo);
}
statusMsg.textContent = '⏳ Отправка рассылки...';
statusMsg.className = 'status-msg';
statusMsg.style.display = 'block';
try {
const response = await fetch('/api/broadcast/create', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
statusMsg.textContent = '✅ ' + result.message;
statusMsg.className = 'status-msg success';
statusMsg.style.display = 'block';
// Очистка формы
document.getElementById('broadcastText').value = '';
document.getElementById('broadcastPhoto').value = '';
// Перезагрузка через 2 секунды
setTimeout(() => location.reload(), 2000);
} else {
statusMsg.textContent = '❌ ' + (result.error || 'Ошибка отправки');
statusMsg.className = 'status-msg error';
statusMsg.style.display = 'block';
}
} catch (error) {
statusMsg.textContent = '❌ Ошибка: ' + error.message;
statusMsg.className = 'status-msg error';
statusMsg.style.display = 'block';
}
}
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>
{% endblock %}