301 lines
15 KiB
HTML
301 lines
15 KiB
HTML
{% 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()">×</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()">×</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 %}
|