domovoy_bot/web/templates/broadcast.html

525 lines
26 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; }
.nav-tabs .nav-link { color: #495057; }
.nav-tabs .nav-link.active { font-weight: bold; color: #667eea; }
.broadcast-item { background: white; padding: 15px; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.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); }
.readers-table { font-size: 0.9em; }
.readers-table th { background: #f8f9fa; }
</style>
{% endblock %}
{% block content %}
<h2 class="mb-4"><i class="bi bi-broadcast"></i> Рассылки</h2>
<!-- Табы -->
<ul class="nav nav-tabs mb-4" id="broadcastTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="create-tab" data-bs-toggle="tab" data-bs-target="#create" type="button" role="tab">
<i class="bi bi-plus-circle"></i> Создать рассылку
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="history-tab" data-bs-toggle="tab" data-bs-target="#history" type="button" role="tab">
<i class="bi bi-clock-history"></i> История и статистика
{% if broadcasts %}
<span class="badge bg-primary ms-1">{{ broadcasts|length }}</span>
{% endif %}
</button>
</li>
</ul>
<div class="tab-content" id="broadcastTabContent">
<!-- ВКЛАДКА: Создание рассылки -->
<div class="tab-pane fade show active" id="create" role="tabpanel">
<div class="card">
<div class="card-body">
<!-- Тип рассылки -->
<label class="form-label fw-bold">Тип рассылки:</label>
<div class="broadcast-type-selector">
<div class="broadcast-type-option active" 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="smart">
<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="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-info">🖼️ Использовать готовое фото:</label>
<select class="form-select bg-dark text-info border-info" id="broadcastArtifactPhoto">
<option value="">-- Не выбрано (или загрузить новое) --</option>
{% for a in artifacts if a.file_type == 'photo' %}
<option value="{{ a.file_id }}">{{ a.filename }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-warning">📄 Использовать готовый документ:</label>
<select class="form-select bg-dark text-warning border-warning" id="broadcastArtifactDocument">
<option value="">-- Не выбрано (или загрузить новое) --</option>
{% for a in artifacts if a.file_type == 'document' %}
<option value="{{ a.file_id }}">{{ a.filename }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Загрузить новую картинку</label>
<input type="file" class="form-control" id="broadcastPhoto" accept="image/*" onchange="previewImage()">
<div id="preview"><img id="previewImg" alt="Предпросмотр"></div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Загрузить новый документ (PDF, DOC)</label>
<input type="file" class="form-control" id="broadcastDocument" accept=".pdf,.doc,.docx,.txt,.xls,.xlsx">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Кому отправить:</label>
<select class="form-select" id="broadcastRecipients" onchange="toggleRecipientFields()">
<option value="all_and_chat">✅ Всем верифицированным + в чат</option>
<option value="all_verified">✅ Только верифицированным в личку</option>
<option value="ig_only">👥 Только Инициативной Группе</option>
<option value="single_user">👤 Конкретному жильцу</option>
</select>
</div>
<div class="mb-3" id="singleUserContainer" style="display: none;">
<label class="form-label text-danger">ID Пользователя (Telegram ID):</label>
<input type="number" class="form-control border-danger" id="targetUserId" placeholder="Например: 123456789">
</div>
</div>
<div class="col-md-6">
<div class="mb-3" id="staircaseContainer">
<label class="form-label">Фильтр по подъезду:</label>
<select class="form-select" id="broadcastStaircase">
<option value="all">🏠 Весь дом (Все подъезды)</option>
<option value="1">🚪 Подъезд 1</option>
<option value="2">🚪 Подъезд 2</option>
<option value="3">🚪 Подъезд 3</option>
<option value="4">🚪 Подъезд 4</option>
<option value="5">🚪 Подъезд 5</option>
<option value="6">🚪 Подъезд 6</option>
</select>
</div>
</div>
</div>
<!-- Опции умной рассылки (всегда включены) -->
<div class="smart-options active">
<h5 class="mb-3"><i class="bi bi-stars"></i> Возможности умной рассылки</h5>
<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>Просмотреть общую статистику во вкладке "История"</li>
</ul>
</div>
</div>
<button type="button" class="btn btn-outline-secondary btn-lg mt-3 me-2" onclick="testBroadcast()">
<i class="bi bi-shield-check"></i> 🧪 Тест (без отправки)
</button>
<button type="submit" class="btn btn-primary btn-lg mt-3">
<i class="bi bi-send"></i> ✨ Отправить умную рассылку
</button>
</form>
<div id="result" class="mt-4"></div>
</div>
</div>
<div class="card mt-4">
<div class="card-body">
<h5>Как использовать:</h5>
<ol>
<li>Введите текст объявления</li>
<li>Прикрепите картинку (необязательно)</li>
<li>Выберите получателей</li>
<li>Нажмите "Отправить умную рассылку"</li>
</ol>
</div>
</div>
</div>
<!-- ВКЛАДКА: История и статистика -->
<div class="tab-pane fade" id="history" role="tabpanel">
{% if broadcasts %}
<div id="broadcastsList">
{% for b in broadcasts %}
<div class="broadcast-item">
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<span class="badge {{ 'badge-smart' if b.broadcast_type == 'smart' else 'badge-regular' }}">
{{ '✨ Умная' if b.broadcast_type == 'smart' else '📢 Обычная' }}
</span>
<strong class="ms-2">#{{ b.id }}</strong>
</div>
<small class="text-muted">{{ b.sent_at }}</small>
</div>
<p class="mb-2">{{ b.text[:150] }}{{ '...' if b.text|length > 150 else '' }}</p>
<div class="d-flex justify-content-between align-items-center">
<div>
<small class="text-muted">
📤 Отправлено: <b>{{ b.total_sent }}</b> |
✅ Прочитали: <b>{{ b.read_count }}</b> |
Не прочитали: <b>{{ b.unread_count }}</b>
</small>
{% if b.total_sent > 0 %}
<div class="progress-mini mt-1">
<div class="progress-mini-fill" style="width: {{ b.read_percent }}%"></div>
</div>
<small class="ms-1">{{ b.read_percent }}%</small>
{% endif %}
</div>
{% if b.broadcast_type == 'smart' %}
<div>
<button class="btn btn-sm btn-outline-success" onclick="showReaders({{ b.id }}, '{{ b.text[:50] }}')">
<i class="bi bi-eye"></i> Кто прочитал
</button>
{% if not b.is_reminder_sent %}
<button class="btn btn-sm btn-outline-warning" onclick="sendReminder({{ b.id }})">
<i class="bi bi-bell"></i> Напомнить
</button>
{% else %}
<small class="text-muted ms-2"><i class="bi bi-check-circle"></i> Напоминание отправлено</small>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Рассылок пока нет. Создайте первую во вкладке "Создать рассылку"!
</div>
{% endif %}
</div>
</div>
<!-- Модальное окно: кто прочитал -->
<div class="modal fade" id="readersModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="readersModalTitle">📊 Кто прочитал</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<ul class="nav nav-tabs mb-3" id="readersTabs" role="tablist">
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#readersRead">✅ Прочитали</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#readersUnread">Не прочитали</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="readersRead">
<div id="readersList" class="readers-table">Загрузка...</div>
</div>
<div class="tab-pane fade" id="readersUnread">
<div id="nonReadersList" class="readers-table">Загрузка...</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
let currentBroadcastId = null;
// Показ/Скрытие полей получателей
function toggleRecipientFields() {
const recipients = document.getElementById('broadcastRecipients').value;
const singleUserContainer = document.getElementById('singleUserContainer');
const staircaseContainer = document.getElementById('staircaseContainer');
if (recipients === 'single_user') {
singleUserContainer.style.display = 'block';
staircaseContainer.style.display = 'none';
} else if (recipients === 'ig_only') {
singleUserContainer.style.display = 'none';
staircaseContainer.style.display = 'none';
} else {
singleUserContainer.style.display = 'none';
staircaseContainer.style.display = 'block';
}
}
// Выбор типа рассылки (теперь только умная)
function selectBroadcastType(type) {
document.getElementById('broadcastType').value = type;
}
// Предпросмотр изображения
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';
}
}
// Тестовая рассылка (без отправки!)
async function testBroadcast() {
const text = document.getElementById('broadcastText').value;
const recipients = document.getElementById('broadcastRecipients').value;
const staircase = document.getElementById('broadcastStaircase').value;
const targetUserId = document.getElementById('targetUserId').value;
const resultDiv = document.getElementById('result');
if (!text.trim()) {
alert('Введите текст рассылки!');
return;
}
if (recipients === 'single_user' && !targetUserId) {
alert('Введите ID пользователя!');
return;
}
resultDiv.innerHTML = '<div class="alert alert-info">🧪 Тестирование (без отправки)...</div>';
const formData = new FormData();
formData.append('text', text);
formData.append('recipients', recipients);
if (recipients !== 'single_user' && recipients !== 'ig_only') {
formData.append('staircase', staircase);
}
if (recipients === 'single_user') {
formData.append('target_user_id', targetUserId);
}
formData.append('has_read_button', 'true');
formData.append('dry_run', 'true');
try {
const r = await fetch('/api/broadcast/create', { method: 'POST', body: formData });
const result = await r.json();
if (result.success) {
resultDiv.innerHTML = `
<div class="alert alert-success">
✅ <b>Тест успешен!</b><br><br>
📊 Рассылка получила бы: <b>${result.recipients_count} пользователей</b><br>
👥 Получатели: ${recipients}<br><br>
<i>Сообщения НЕ отправлялись</i>
</div>
`;
} else {
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${result.error || 'Неизвестная'}</div>`;
}
} catch (err) {
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${err.message}</div>`;
}
}
// Отправка формы
document.getElementById('broadcastForm').addEventListener('submit', async (e) => {
e.preventDefault();
const text = document.getElementById('broadcastText').value;
const photo = document.getElementById('broadcastPhoto').files[0];
const doc = document.getElementById('broadcastDocument').files[0];
const artifactPhotoId = document.getElementById('broadcastArtifactPhoto').value;
const artifactDocumentId = document.getElementById('broadcastArtifactDocument').value;
const recipients = document.getElementById('broadcastRecipients').value;
const staircase = document.getElementById('broadcastStaircase').value;
const targetUserId = document.getElementById('targetUserId').value;
const resultDiv = document.getElementById('result');
if (!text.trim()) {
alert('Введите текст рассылки!');
return;
}
if (recipients === 'single_user' && !targetUserId) {
alert('Введите ID пользователя!');
return;
}
// Двойное подтверждение для реальной отправки
const confirmed = confirm(
'⚠️ ВНИМАНИЕ! Это отправит сообщения реальным пользователям!\n\n' +
`Получатели: ${recipients}\n` +
(recipients !== 'single_user' && recipients !== 'ig_only' ? `Подъезд: ${staircase === 'all' ? 'Весь дом' : staircase}\n\n` : '\n') +
'Вы уверены?'
);
if (!confirmed) return;
resultDiv.innerHTML = '<div class="alert alert-info">📤 Отправка рассылки...</div>';
const formData = new FormData();
formData.append('text', text);
formData.append('recipients', recipients);
if (recipients !== 'single_user' && recipients !== 'ig_only') {
formData.append('staircase', staircase);
}
if (recipients === 'single_user') {
formData.append('target_user_id', targetUserId);
}
formData.append('has_read_button', 'true');
formData.append('track_reads', 'true');
if (artifactPhotoId) {
formData.append('artifact_photo_id', artifactPhotoId);
}
if (artifactDocumentId) {
formData.append('artifact_document_id', artifactDocumentId);
}
if (photo && !artifactPhotoId) {
formData.append('photo', photo);
}
if (doc && !artifactDocumentId) {
formData.append('document', doc);
}
try {
const r = await fetch('/api/broadcast/create', { method: 'POST', body: formData });
const result = await r.json();
if (result.success) {
resultDiv.innerHTML = `<div class="alert alert-success">✅ ${result.message}<br><br>📊 Перейдите во вкладку "История и статистика" для просмотра результатов →</div>`;
document.getElementById('broadcastForm').reset();
document.getElementById('preview').style.display = 'none';
// Перезагружаем страницу через 2 секунды чтобы обновить историю
setTimeout(() => location.reload(), 2000);
} 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 showReaders(broadcastId, title) {
currentBroadcastId = broadcastId;
document.getElementById('readersModalTitle').textContent = '📊 ' + title;
const modal = new bootstrap.Modal(document.getElementById('readersModal'));
modal.show();
// Загружаем данные
try {
const [readersResp, nonReadersResp] = await Promise.all([
fetch(`/api/broadcast/readers/${broadcastId}`),
fetch(`/api/broadcast/non_readers/${broadcastId}`)
]);
const readers = await readersResp.json();
const nonReaders = await nonReadersResp.json();
// Прочитали
if (readers.readers && readers.readers.length > 0) {
document.getElementById('readersList').innerHTML = readers.readers.map(r => `
<div class="d-flex justify-content-between p-2 border-bottom">
<div>
<strong>${r.name || 'user' + r.user_id}</strong>
${r.apartment ? `<span class="text-muted">(кв. ${r.apartment})</span>` : ''}
</div>
<small class="text-muted">${r.read_at}</small>
</div>
`).join('');
} else {
document.getElementById('readersList').innerHTML = '<p class="text-muted">Пока никто не прочитал</p>';
}
// Не прочитали
if (nonReaders.non_readers && nonReaders.non_readers.length > 0) {
document.getElementById('nonReadersList').innerHTML = nonReaders.non_readers.map(r => `
<div class="d-flex justify-content-between p-2 border-bottom">
<div>
<strong>${r.name || 'user' + r.user_id}</strong>
${r.apartment ? `<span class="text-muted">(кв. ${r.apartment})</span>` : ''}
</div>
</div>
`).join('');
} else {
document.getElementById('nonReadersList').innerHTML = '<p class="text-success"><i class="bi bi-check-circle"></i> Все прочитали!</p>';
}
} catch (err) {
console.error('Ошибка загрузки данных:', err);
}
}
// Отправить напоминание
async function sendReminder(broadcastId) {
if (!confirm('Отправить напоминание непрочитавшим?')) return;
try {
const resp = await fetch(`/api/broadcast/send_reminder/${broadcastId}`, { method: 'POST' });
const result = await resp.json();
if (result.success) {
alert('✅ ' + result.message);
location.reload();
} else {
alert('❌ Ошибка: ' + (result.error || 'Неизвестная'));
}
} catch (err) {
alert('❌ Ошибка: ' + err.message);
}
}
</script>
{% endblock %}